【Java多线程】Thread类及常见方法;线程安全问题;多线程的基本案例

【Java多线程】Thread类及常见方法;线程安全问题;多线程的基本案例,第1张

多线程

文章目录
  • 多线程
    • 1. 认识线程
      • 1.1 概念
        • 线程是什么?
        • 为什么要有线程
        • 进程和线程的区别
      • 1.2 创建线程
        • 继承Thread类
        • 实现Runnable接口
        • 其他变形方式
      • 1.3 多线程的优势
    • 2. Thread类及常见方法
      • 2.1 Thread的常见构造方法
      • 2.2 Thread 的常见属性
      • 2.3 启动一个线程
      • 2.4 中断一个线程
      • 2.5 等待一个线程
      • 2.6 获取当前线程引用
      • 2.7 休眠当前线程
    • 3. 线程的状态
      • 3.1 观察线程的所有状态
      • 3.2 观察线程的状态和转移
    • 4. 线程安全问题
      • 4.1 线程安全的概念
      • 4.2 观察线程不安全
      • 4.3 线程不安全的原因
      • 4.4 解决上面遇到的线程不安全问题
    • 5. Synchronized关键字
      • 5.1 Synchronized的特性
      • 5.2 synchronized使用示例
      • 5.3 Java标准库中的线程安全类
    • 6. volatile关键字
      • volatile修饰的变量,能够保证"内存可见性"
      • volatile不保证原子性
      • synchronized也能保证内存可见性
    • 7. wait和notify
      • 7.1 wait()方法
      • 7.2 notify()方法
      • 7.3 notifyAll()方法
      • 7.4 wait和sleep的对比
    • 8. 多线程案例
      • 8.1 单例模式
      • 8.2 阻塞式队列
      • 8.3 定时器
      • 8.4 线程池
    • 9. 保证线程安全的思路
    • 10. 对比线程和进程

1. 认识线程 1.1 概念 线程是什么?

线程也称"轻量级进程",一个线程就是一个"执行流",每个线程之间都可以按照顺序执行自己的代码,多个线程之间"同时"执行着多份代码

为什么要有线程

1.“并发编程”,成为"刚需".

  • 单核CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU. 而并发编程能更充分利用多核 CPU 资源.
  • 有些任务场景需要 “等待 IO”, 为了让等待 IO 的时间能够去做一些其他的工作, 也需要用到并发编程.

2. 虽然多进程也能实现并发编程,但是线程比进程更轻量

  • 创建线程比创建进程更快
  • 销毁线程比销毁进程更快
  • 调度线程比调度进程更快
  • 创建线程不用申请资源,销毁线程也不用释放资源
进程和线程的区别
  • 进程包含线程,一个进程可以包含一个或多个线程。
  • 每个进程至少有一个线程存在,即主线程
  • 进程是系统分配资源的最小单位,线程是系统调度的最小单位
  • 每个进程都有独立的内存空间(虚拟地址空间),同一个进程的多个线程之间,共用一个虚拟地址空间
1.2 创建线程 继承Thread类
class MyThread extends Thread{
    @Override
    public void run() {
            System.out.println("hello thread");
    }
}
public class Main{
    public static void main(String[] args) {
        Thread t = new MyThread();//创建MyThread类的实例
        t.start();//线程开始运行
    }
}
实现Runnable接口
class MyRunnable implements Runnable{
        @Override
    public void run() {
        System.out.println("hello thread");
   }
}
public class Main{
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable());
        t.start();
    }
}

对比上面两种方法:
继承 Thread 类, 直接使用 this 就表示当前线程对象的引用.
实现 Runnable 接口, this 表示的是 MyRunnable 的引用. 需要使用 Thread.currentThread()

其他变形方式

匿名内部类创建Thread子类对象

public class Main{
    public static void main(String [] args){
        Thread t = new Thread(){
        	@Override
        	public void run(){
            	System.out.println("hello thread");  
        	}
        };
        t.start();
    }
}

匿名内部类创建Runnable子类对象

public class Main{
    public static void main(String [] args){
		Thread t = new Thread(new Runnable()){
             @Override
            public void run() {
        		System.out.println("使用匿名类创建 Runnable 子类对象");
   			}
        };
        t.start();
    }
}

lambda表达式创建Runnable子类对象

public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
               System.out.println("hello thread");
        });
        t.start();
    }
}
1.3 多线程的优势

可以观察多线程在一些场合下是可以提高程序的整体运行效率的。

  • 使用 System.nanoTime() 可以记录当前系统的 纳秒 级时间戳.
  • serial 串行的完成一系列运算. concurrency 使用两个线程并行的完成同样的运算.
public class ThreadAdvantage {
    // 多线程并不一定就能提高速度,可以观察,count 不同,实际的运行效果也是不同的
    private static final long count = 10_0000_0000;
    public static void main(String[] args) throws InterruptedException {
        // 使用并发方式
        concurrency();
        // 使用串行方式
        serial();
   }
    private static void concurrency() throws InterruptedException {
        long begin = System.nanoTime();
        
        // 利用一个线程计算 a 的值
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                int a = 0;
                for (long i = 0; i < count; i++) {
                    a--;
               }
           }
       });
        thread.start();
        // 主线程内计算 b 的值
        int b = 0;
        for (long i = 0; i < count; i++) {
            b--;
       }
        // 等待 thread 线程运行结束
        thread.join();
        
        // 统计耗时
        long end = System.nanoTime();
        double ms = (end - begin) * 1.0 / 1000 / 1000;
        System.out.printf("并发: %f 毫秒%n", ms);
   }
    private static void serial() {
        // 全部在主线程内计算 a、b 的值
        long begin = System.nanoTime();
        int a = 0;
        for (long i = 0; i < count; i++) {
            a--;
       }
        int b = 0;
        for (long i = 0; i < count; i++) {
			b--;
       }
        long end = System.nanoTime();
        double ms = (end - begin) * 1.0 / 1000 / 1000;
        System.out.printf("串行: %f 毫秒%n", ms);
   }
}
//并发: 399.651856 毫秒
//串行: 720.616911 毫秒
2. Thread类及常见方法

Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关
联。
用我们上面的例子来看,每个执行流,也需要有一个对象来描述,类似下图所示,而 Thread 类的对象
就是用来描述一个线程执行流的,JVM 会将这些 Thread 对象组织起来,用于线程调度,线程管理。

2.1 Thread的常见构造方法
方法说明
Thread()创建线程对象
Thread(Runnable target)使用Runnable对象创建线程对象
Thread(String name)创建线程对象,并为线程命名
Thread(Runnable target,String name)使用Runnable对象创建线程对象,并为线程命名
2.2 Thread 的常见属性
属性获取方法
IDgetId()
名称getName()
状态getState()
优先级getPriority()
是否后台线程idDaemon()
是否存活isAlive()
是否被中断isInterrupted()
  • ID是线程的唯一标识,不同线程不会重复
  • 名称用于调试
  • 状态表示线程当前所处的一个情况
  • 优先级高的线程理论上更容易被调度到
  • JVM会在一个进程的所有非后台线程结束后,才会结束运行(创建一个线程,默认不是后台线程)
  • 简单理解为run方法是否运行结束了
2.3 启动一个线程

调用 start 方法, 才真的在 *** 作系统的底层创建出一个线程.

2.4 中断一个线程

两种常见的方式:

  1. 通过共享的标记来进行沟通
  2. 调用interrupt()方法来通知

示例1:

public class Main{
    public static boolean flg = true;
    public static void main(String [] args){
        Thread t = new Thread(){
            @Override
            public void run(){
                while(flg){
                    System.out.pinrtln("hello thread");
                    try{
                        Thread.sleep(1000);
                    }catch(InterruptedException e){
                        e.printStackTrace();
                    }
                }
                System.out.println("线程结束");
            }
        };
        t.start();
        Thread.sleep(3000);//三秒后将标志位置为false,线程就会中断
        flg = false;
    }
}
方法说明
public void interrupt()中断对象关联的线程,如果线程正在阻塞,则以异常的方式通知,否则设置标志位
public static boolean interrupted()判断当前线程的中断标志位是否设置,调用后清除标志位
public boolean isInterrupted()判断对象关联的线程的标志位是否设置,调用后不清除标志位

示例2:

使用thread对象的interrupted()方法通知线程结束

 public static void main(String[] args) {
        Thread t = new Thread(){
            @Override
            public void run() {
                //Thread.currentThread().isInterrupted()默认是false 表示未被中断
                while(!Thread.currentThread().isInterrupted()){
                    System.out.println("线程运行中");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        break;
                    }
                }
            }
        };
        t.start();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.interrupt();
     //将Thread.currentThread().isInterrupted()设置为TRUE 表示线程中断
    }

调用interrupt()方法后,thread收到通知的方式有两种:

  1. 如果线程因为调用wait/join/sleep等方法而阻塞挂起,则以 InterruptedException异常的形式通知,清除中断标志,

    当出现 InterruptedException的时候,要不要结束线程取决于catch中代码的写法,可以选择忽略这个异常,也可以跳出循环结束线程

  2. 否则,只是内部的一个中断标志被设置,thread可以通过

    Thread.interrupted()判断当前线程的中断标志被设置,清除中断标志,

    Thread.currentThread().isInterrupted()判断指定线程的中断标志被设置,不清除中断标志,这种方式通知收到的更及时,即使线程真正sleep也可以马上收到

示例3:

观察标志位是否清除

标志位是否清除, 就类似于一个开关.
Thread.isInterrupted() 相当于按下开关, 开关自动d起来了. 这个称为 “清除标志位”
Thread.currentThread().isInterrupted() 相当于按下开关之后, 开关d不起来, 这个称为"不清除标志位"

使用Thread.isInterrupted(),线程中断会清除标志位

public class Main{
      private static class MyRunnable implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.interrupted());
           }
       }
   }
    public static void main(String[] args) throws InterruptedException {
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "李四");
        thread.start();
        thread.interrupt();
   }
}
//只有一开始是true,之后都是false,因为标志位被清 

使用Thread.currentThread().isInterrupted(),线程中断标记为不会清除

public class ThreadDemo {
    private static class MyRunnable implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
 System.out.println(Thread.currentThread().isInterrupted());
           }
        }
   }
    public static void main(String[] args) throws InterruptedException {
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "李四");
        thread.start();
        thread.interrupt();
   }
}
//全都是true,因为标志位没有被清除 输出10行true

2.5 等待一个线程

有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。

    public static void main(String[] args) throws InterruptedException {
        Runnable target = ()->{
                for (int i = 0; i < 3; i++) {
                    try {
                        System.out.println(Thread.currentThread().getName()+ ":我正在工作");
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName()+":工作结束");
        };
        Thread t1 = new Thread(target,"李四");
        Thread t2 = new Thread(target,"老六");
        System.out.println("先让李四工作");
        t1.start();//t1线程启动
        t1.join();//等待t1线程结束
        System.out.println("李四工作结束");
        t2.start();//t2线程启动
        t2.join();//等待t2线程结束
        System.out.println("老六工作结束");
    }
//执行结果
先让李四工作
李四:我正在工作
李四:我正在工作
李四:我正在工作
李四:工作结束
李四工作结束
老六:我正在工作
老六:我正在工作
老六:我正在工作
老六:工作结束
老六工作结束

方法说明
public void join()等待线程结束
public void join(long millis)等待线程结束,最多等millis毫秒
public void join(long millis,int nanos)等待线程结束,最多等millis毫秒,nanos纳秒。精度更高
2.6 获取当前线程引用
public class Main{
    public static void main(Stirng [] args){
        Thred t = Thread.currentThread();
        System.out.println(t.getName());
    }
}
//Thread.currentThread() 返回当前线程对象的引用

2.7 休眠当前线程

因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的。

方法说明
public static void sleep(long millis) throws InterruptedException休眠当前线程 millis毫秒
public static void sleep(long millis, int nanos) throwsInterruptedException可以更高精度的休眠
public static void main(String[] args) throws InterruptedException {
        System.out.println(System.currentTimeMillis());
        Thread.sleep(3 * 1000);
        System.out.println(System.currentTimeMillis());
   }

3. 线程的状态 3.1 观察线程的所有状态

线程的状态是一个枚举类型Thread.State

  public static void main(String[] args) {
        for (Thread.State state : Thread.State.values()) {
            System.out.println(state);
       }
   }
//执行结果
NEW
RUNNABLE
BLOCKED
WAITING
TIMED_WAITING
TERMINATED

  • NEW:表示Thread对象创建了,但 *** 作系统内核中的PCB还没创建,当调用.start()方法后,才会创建
  • RUNNABLE:PCB对象创建了,随时待命。这个线程可能在CPU上运行,也可能在就绪队列中排队
  • BLOCKED:线程中尝试加锁,但锁被其他线程占用,此时这个线程也会处于阻塞等待状态,这个等待会在其他线程释放锁之后被唤醒
  • WAITING:线程中如果调用了wait()方法,也会处于阻塞等待状态,WAITING等待没有等待结束时间,会一直等待,除非被其他线程唤醒
  • TIMED_WAITING:表示当前的PCB在阻塞队列中等待,这个等待有等待结束时间。Thread.sleep()会触发这个状态
  • TERMINATED:表示当前PCB结束了,Thread对象还在,此时调用获取状态就会得到这个状态
  • BLOCKED,WAITING,TIMED_WAITING 这三个都表示线程阻塞等待状态
3.2 观察线程的状态和转移
public static void main(String[] args) {
        final Object object = new Object();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (object) {
                    while (true) {
                        try {
                            Thread.sleep(1000);
                            //将sleep换成wait后,t1的状态是WAITING
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }, "t1");
        t1.start();
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (object) {
                    System.out.println("hehe");
                }
            }
        }, "t2");
        t2.start();
    }

使用 jconsole 可以看到 t1 的状态是 TIMED_WAITING , t2 的状态是 BLOCKED

BLOCKED 表示等待获取锁, WAITING 和 TIMED_WAITING 表示等待其他线程发来通知.

TIMED_WAITING 线程在等待唤醒,但设置了时限; WAITING 线程在无限等待唤醒

4. 线程安全问题 4.1 线程安全的概念

如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的

4.2 观察线程不安全
static class Counter {
    public int count = 0;
    void increase() {
        count++;
   }
}
public static void main(String[] args) throws InterruptedException {
    final Counter counter = new Counter();
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 50000; i++) {
            counter.increase();
       }
   });
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 50000; i++) {
            counter.increase();
       }
   });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(counter.count);
}

线程1和线程2分别对调用increase()方法,对count自增50000次,本来我们预期count会自增到100000,但是当线程1,线程2都执行完毕后,多次打印count的值,发现结果并不确定,结果范围在50000-100000之间,此时就发生了线程不安全

4.3 线程不安全的原因
  1. *** 作系统调度线程的时候,采用"抢占式"的方式,所以具体线程被CPU调度的顺序是不确定的。

  2. 多个线程同时修改同一个内存数据

  3. 原子性

    我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。

    一条 java 语句不一定是原子的,也不一定只是一条指令

    像上面代码中的count++ *** 作就不是原子性的,count++ *** 作包含3个步骤,1.从内存中读取数据,2.修改数据,3.再将数据写到CPU

    如果一个线程正在对一个变量 *** 作,如果这个 *** 作被打断了,结果可能就是错误的,

  4. 内存可见性

    内存可见性指, 一个线程对共享变量值的修改,能够及时地被其他线程看到.

    Java 内存模型 (JMM): Java虚拟机规范中定义了Java内存模型.
    目的是屏蔽掉各种硬件和 *** 作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果.

  1. 指令重排序

    一段代码是这样的:

    1. 去前台取下 U 盘

    2. 去教室写 10 分钟作业

    3. 去前台取下快递

      如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题,可以少跑一次前台。这种叫做指令重排序

      编译器对于指令重排序的前提是 “保持逻辑不发生变化”. 这一点在单线程环境下比较容易判断, 但是在多线程环境下就没那么容易了, 多线程的代码执行复杂程度更高, 编译器很难在编译阶段对代码的执行效果进行预测, 因此激进的重排序很容易导致优化后的逻辑和之前不等价

4.4 解决上面遇到的线程不安全问题
static class Counter {
    public int count = 0;
    synchronized void increase() {
        count++;
   }
}
public static void main(String[] args) throws InterruptedException {
     final Counter counter = new Counter();
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 50000; i++) {
            counter.increase();
       }
   });
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 50000; i++) {
            counter.increase();
       }
   });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(counter.count);
}

5. Synchronized关键字 5.1 Synchronized的特性
  • 互斥

    synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到
    同一个对象 synchronized 就会阻塞等待.
    进入 synchronized 修饰的代码块, 相当于 加锁
    退出 synchronized 修饰的代码块, 相当于 解锁

    针对每一把锁, *** 作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝试进行加锁, 就加不上了, 就会阻塞等待, 一直等到之前的线程解锁之后, 由 *** 作系统唤醒一个新的线程, 再来获取到这个锁.

    相当于将多个线程的并发执行,强制修改为串行执行,虽然会降低执行效率,但是提高效率的前提是要保证数据的正确性

    注意:
    上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠 *** 作系统来 “唤醒”. 这也就是 *** 作系统线程调度的一部分工作.
    假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则.

  • 刷新内存,保证了内存可见性

    synchronized的工作过程

    1.获得互斥锁

    2.从主内存拷贝变量的最新副本到工作的内存

    3.执行代码

    4.将更改后的共享变量的值刷新到主内存

    5.释放互斥锁

    相当于每执行一次代码,都会写入主内存,下一次执行时,再从主内存读取数据

  • 可重入

    synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;

    假如线程1已经对一个代码块进行了加锁,第二次加锁时,就会阻塞等待,直到第一次的锁被释放时,才能获得到第二个锁,但是第一个锁是由线程1执行的,无法进行解锁 *** 作,这时候就会死锁

    synchronized void increase() {//第一次加锁
        synchronized(this){//等待第一次解锁后才能加锁
            count++;
        }
    }//执行到这一行才会第一次解锁
    //此时就出现了死锁
    
    

    但是Java 中的 synchronized 是 可重入锁, 因此没有上面的问题.

    static class Counter {
        public int count = 0;
        synchronized void increase() {
            count++;
       }
        synchronized void increase2() {
            increase();
       }
    }
    
    

    increase和increase2两个方法都加了synchronized,此处的 synchronized 都是针对 this 当前对象加锁的,在调用 increase2 的时候, 先加了一次锁, 执行到 increase 的时候, 又加了一次锁. (上个锁还没释放, 相当于连续加两次锁

    在可重入锁的内部, 包含了 “线程持有者” 和 “计数器” 两个信息.
    如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取到锁, 并让计数器自增.
    解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)

5.2 synchronized使用示例

synchronized 本质上要修改指定对象的 “对象头”. 从使用角度来看, synchronized 也势必要搭配一个具体的对象来使用

1、直接修饰普通方法: 锁的 SynchronizedDemo 对象

public class SynchronizedDemo {
    public synchronized void methond() {
   }
}

2、修饰静态方法:锁的SynchronizedDemo类的对象

public class SynchronizedDemo {
    public synchronized static void method() {
   }
}

3、修饰代码块:明确指定锁哪个对象

锁当前对象

public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            
       }
   }
}

锁类对象

public class SynchronizedDemo {
    public void method() {
        synchronized (SynchronizedDemo.class) {
       }
   }
}

注意:两个线程竞争同一把锁时,才会产生阻塞等待,两个线程分别获取两把不同的锁时,不会产生竞争

5.3 Java标准库中的线程安全类

Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施.

ArrayList、LinkedList、HashMap、TreeMap、HashSet、TreeSet、StringBuilder

但是还有一些是线程安全的. 使用了一些锁机制来控制

Vector、HashTable、ConcurrentHashMap、StringBuffer

String类也是线程安全的,但是没有加锁,因为String是不可修改的

6. volatile关键字 volatile修饰的变量,能够保证"内存可见性"

代码在写入volatile修饰的变量的时候,先改变线程工作内存中volatile变量副本的值,再将改变后的副本的值从工作内存中刷新到主内存中

代码在读取volatile修饰的变量的时候,先从主内存中读取volatile变量的最新值到线程的工作内存中,再从工作内存中读取volatile变量的副本

前面我们讨论内存可见性时说了, 直接访问工作内存(实际是 CPU 的寄存器或者 CPU 的缓存), 速度非常快, 但是可能出现数据不一致的情况.
加上 volatile , 强制读写内存. 速度是慢了, 但是数据变的更准确了

代码示例:

创建两个线程t1,t2,t1中包含一个循环,这个循环以flg==0为循环条件,t2中从键盘读入一个整数,并将这个整数赋值给flg,我们预期当用户输入非0值时,t1线程结束

public class Test {
    static class Counter{
         public int flg = 0;
    }

    public static void main(String[] args) {
        Counter counter = new Counter();
        Thread t1 = new Thread(()->{
           while (counter.flg == 0){
           }
            System.out.println("循环结束");
        });
        Thread t2 = new Thread(()->{
            Scanner scanner = new Scanner(System.in);
            System.out.println("输入一个整数");
            counter.flg = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}
//此时当用户输入非0值时,t1线程循环不会结束。因为t1线程中的循环在快速的读取flg的值,由于读取的太快了,所以编译前进行了优化,只从内存中读取一次,也就是0,之后的循环读取flg的值只在自己的工作内存(也就是CPU的寄存器)中读取,此时t2对flg值进行修改,t1感知不到flg的变化

给flg 加上 volatile后,用户输入非0 值时,t1线程循环能够立即结束

volatile不保证原子性

volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性, volatile 保证的是内存可见

static class Counter {
    volatile public int count = 0;
    void increase() {
        count++;
   }
}
public static void main(String[] args) throws InterruptedException {
    final Counter counter = new Counter();
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 50000; i++) {
            counter.increase();
       }
   });
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 50000; i++) {
            counter.increase();
       }
   });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(counter.count);
}

此时可以看到, 最终count的值仍然无法保证时100000

synchronized也能保证内存可见性

synchronized 既能保证原子性, 也能保证内存可见性

static class Counter {
    public int flag = 0;
}
public static void main(String[] args) {
    Counter counter = new Counter();
    Thread t1 = new Thread(() -> {
        while (true) {
            //加上synchronized之后,针对flg的 *** 作,也会读写内存
            synchronized (counter) {
                if (counter.flag != 0) {
                    break;
               }
           }
            // do nothing
       }
        System.out.println("循环结束!");
   });
    Thread t2 = new Thread(() -> {
        Scanner scanner = new Scanner(System.in);
        System.out.println("输入一个整数:");
        counter.flag = scanner.nextInt();
   });
    t1.start();
    t2.start();
}

7. wait和notify

由于线程之间是抢占式执行的,因此线程之间执行的先后顺序难以预知

但是实际开发中,有时候我们希望合理的协调多个线程之间的执行先后顺序

完成这个协调工作主要涉及到三个方法

  • wait() / wait(long timeout):让线程进入等待状态
  • notify / notifyAll():唤醒在当前对象上等待的线程

wait,notify,notifyAll都是Object类的方法

7.1 wait()方法

wait做的事情:

  • 使当前执行代码的线程进行等待(把线程从就绪队列放到等待队列)
  • 释放当前的锁
  • 满足一定条件时被唤醒,重新尝试获取这个锁

注意:wait要搭配synchronized来使用,脱离synchronized使用wait会直接抛出异常

wait结束等待的条件:

  • 其他线程调用该对象的notify()方法
  • wait等待时间超时(wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间)
  • 其他线程调用该等待线程的interrupted方法,导致wait抛出InterruptedException异常

wait()方法的使用

public static void main(String[] args) throws InterruptedException {
    Object object = new Object();
    synchronized (object) {
        System.out.println("等待中");
        object.wait();
        System.out.println("等待结束");
   }
}

这样在执行到object.wait()之后就一直等待下去,那么程序肯定不能一直这么等待下去了。这个时候就需要使用到了另外一个方法唤醒的方法notify()

7.2 notify()方法

notify()方法是唤醒等待的线程

  • notify()方法也要在同步方法或同步块(synchronized)中调用,该方法是用来通知那些可能等待该对象的对象锁的其他线程,对其发出通知notify,使它们重新获取该对象的对象锁
  • 如果有多个线程等待,则由线程调度器随机挑选一个wait状态的线程(没有先来后到)
  • 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁

示例:使用notify()方法唤醒线程

  • 创建 WaitTask 类, 对应一个线程, run 内部调用 wait.
  • 创建 NotifyTask 类, 对应另一个线程, 在 run 内部调用notify
  • 注意, WaitTask 和 NotifyTask 内部持有同一个 Object locker. WaitTask 和 NotifyTask 要想配合
    就需要搭配同一个 Object.
public class ThreadDemo18 {
    static class WaitTask implements Runnable{
        Object locker = null;
        public waitTask(Object locker) {
            this.locker = locker;
        }
        @Override
        public void run() {
            synchronized (locker){
                System.out.println("wait 开始");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("wait 结束");
            }
        }
    }
    static class NotifyTask implements  Runnable{
        Object locker = null;
        public notifyTask(Object locker) {
            this.locker = locker;
        }
        @Override
        public void run() {
            synchronized (locker){
                System.out.println("notify 开始");
                locker.notify();
                System.out.println("notify 结束");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Thread t1 = new Thread(new waitTask(locker));
        Thread t2 = new Thread(new notifyTask(locker));
        t1.start();
        Thread.sleep(3000);//3秒后唤醒t1线程
        t2.start();
    }
}
//执行结果
wait 开始
notify 开始
notify 结束
wait 结束

7.3 notifyAll()方法

notify()方法只是唤醒某一个线程,使用notifyAll()方法可以一次唤醒所有的等待线程

在上面代码的基础上做出修改,创建3个WaitTask 实例,1 个 NotifyTask 实例.

如果调用notify(),只能唤醒一个线程,并且使随机唤醒一个线程,修改NotifyTask中的run方法,把notify()替换成notifyAll()后,直接同时唤醒3个wait中的线程

**注意:虽然是同时唤醒 3 个线程, 但是这 3 个线程需要竞争锁. 所以并不是同时执行, 而仍然是有先有后的执行. **

7.4 wait和sleep的对比

其实理论上 wait 和 sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间,唯一的相同点就是都可以让线程放弃执行一段时间.

sleep使指定一个固定时间来阻塞等待,wait即可以指定时间,也可以无限等待

wait可以通过notify或者interrupt或者等待时间超时来唤醒,sleep通过等待时间超时或interrupt唤醒

wait主要的用途是为了协调多个线程的执行顺序,sleep只是让线程进入休眠状态,并不涉及多个线程的配合

8. 多线程案例 8.1 单例模式

单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例.

饿汉模式:类加载的同时,创建实例

class Singleton{
    private static Singleton instance = new Singleton();
    private Singleton(){}
    public static Singleton getInstance(){
        return instance;
    }
}

线程安全,不涉及多个线程修改同一个变量,只涉及多个线程读取同一个变量

懒汉模式-单线程版

类加载的时候不创建实例,第一次使用的时候才创建实例

class Singleton{
    private static Singleton instance = null;
    private Singleton(){}
    public static Singleton getInstance(){
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}

懒汉模式-多线程版

上面的懒汉模式的实现是线程不安全的

线程安全问题发生在首次创建实例时,getInstance方法并不是原子的,而是可以分为两步,第一步,判断是否是第一次创建实例,第二步,如果是第一次,就修改instance创建实例,所以此时就可能发生线程不安全。如果多个线程同时调用getInstance方法,就可能创建出多个实例,但是如果实例创建好了,后面再在多线程环境调用getInstance方法就不再有线程安全问题了

加上synchronized可以改善这里的线程安全问题

class Singleton{
    private static Singleton instance = null;
    private Singleton() {}
    public synchronized static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
       }
        return instance;
   }
}

加上synchronized之后,虽然解决了线程安全问题,但是线程安全问题只发生在首次创建实例时,后续每次调用getInstance方法时,也都会涉及到加锁 *** 作,加锁 *** 作本身是一个开销较大的 *** 作,也可能会导致代码执行速度降低

以下代码在加锁的基础上, 做出了进一步改动:

  • 使用双重 if 判定, 降低锁竞争的频率.
  • 给 instance 加上了 volatile.
class Singleton {
    private static volatile Singleton instance = null;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
           if (instance == null) {
               instance = new Singleton();
               }
           }
       }
        return instance;
   }
}

改动的解释:外层的 if 就是判定下看当前是否已经把 instance 实例创建出来了. 同时为了避免 “内存可见性” 导致读取的 instance 出现偏差, 于是补充上 volatile . 当多线程首次调用 getInstance, 大家可能都发现 instance 为 null, 于是又继续往下执行来竞争锁, 其中竞争成功的线程, 再完成创建实例的 *** 作. 当这个实例创建完了之后, 其他竞争到锁的线程就被里层 if 挡住了. 也就不会继续创建其他实例.

8.2 阻塞式队列

阻塞队列是一种特殊的机制,也蹲守"先进先出"的原则

阻塞队列能是一种线程安全的数据结构, 并且具有以下特性:

  • 当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素.
  • 当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素.

阻塞队列的一个典型应用场景就是 “生产者消费者模型”. 这是一种非常典型的开发模型

生产者消费者模型

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。

生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用

待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取.

  1. 阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力.

  2. 阻塞队列也能使生产者和消费者之间 解耦.

标准库中的阻塞队列

BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.

put 方法用于阻塞式的入队列, take 用于阻塞式的出队列.

BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性

BlockingQueue<String> queue = new LinkedBlockingQueue<>(10);
//阻塞队列可以自动扩容,也可以输入最大范围个数
        queue.put("hello");
        String str = queue.take();
        System.out.println(str);
        str = queue.take();
        System.out.println(str);

生产者消费者模型

public static void main(String[] args) {
        //创建一个阻塞队列作为交易场所
        BlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
        //一个线程为生产者
        Thread customer = new Thread(){
            @Override
            public void run() {
                while (true){
                    try {
                        Integer val = queue.take();
                        System.out.println("消费元素 "+val);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        customer.start();
        //一个线程为消费者
        Thread producer = new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    System.out.println("生产了元素:"+i);
                    try {
                        queue.put(i);
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        producer.start();
        try {
            customer.join();
            producer.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
//由于消费者是不需要等待直接消费,而生产者每过一秒生产一个元素,所以当生产者生产出一个元素后,消费者才能消费一个元素
生产了元素:0
消费元素 0
生产了元素:1
消费元素 1
生产了元素:2
消费元素 2

阻塞队列的实现

  • 通过"循环队列"的方式实现
  • 使用synchronized进行加锁控制
  • put插入元素的时候,判断队列满了,就调用wait进入阻塞等待状态,
  • take取出元素时,判断如果队列为空,也要调用wait进入阻塞等待状态
  • 当插入一个新元素时,会破坏take的阻塞条件(队列空),所以需要notify唤醒take的阻塞
  • 当取出一个元素时,也会破坏put的阻塞条件(队列满),所以需要notify唤醒put的阻塞
static class BlockingQueue{
        private int [] items = new int[1000];//使用数组实现循环队列,相当于队列的最大容量
        private int head = 0;
        private int tail = 0;
        private int size = 0;
        private Object locker = new Object();
        //put入队列
        public void put(int item) throws InterruptedException {
            //如果队列满了,对于阻塞队列来说就要阻塞
            synchronized(locker){
                while (size == items.length){
                    //用while能确保此时队列不满,wait被唤醒后再确认一次队列是否满,虽然我们在take中使用的是notify()唤醒一个线程,但是用while更安全,
                        locker.wait();
                        break;

                }
                items[tail] = item;
                tail++;
                if(tail >= items.length){
                    tail = 0;
                }
                size++;
                //此处的notify用来唤醒take中的wait
                locker.notify();
            }
        }
        //take出队列
        public int take() throws InterruptedException {
            synchronized(locker){
                //如果队列为空,对于阻塞队列来说使用出队列 *** 作就要阻塞
                while (size == 0){
                    //此处使用while和上述原因一致,可以确保线程被唤醒后,再次判断队列不为空
                    locker.wait();
                }
                int ret = items[head];
                head++;
                if(head >= items.length){
                    head = 0;
                }
                size--;
                //此处的notify用来唤醒put中的wait
                locker.notify();
                return ret;
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue queue = new BlockingQueue();
       //消费者线程
        Thread consumer = new Thread(){
            @Override
            public void run() {
                while (true){
                    int ret = 0;
                    try {
                        Thread.sleep(1000);
                        ret = queue.take();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("消费元素 " + ret);
                }
            }
        };
        consumer.start();
        //生产者线程
        Thread producer = new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    System.out.println("生产元素 "+i);
                    try {
                        queue.put(i);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        producer.start();
        consumer.join();
        producer.join();
    }

8.3 定时器

定时器也是软件开发中的一个重要组件. 类似于一个 “闹钟”. 达到一个设定的时间之后, 就执行某个指定
好的代码

标准库中的定时器

  • 标准库中提供了一个Timer类,Timer类的核心方法为schedule
  • schedule包含两个参数,第一个参数指定即将要执行的任务代码,第二个参数指定多长时间之后执行(单位毫秒)
Timer timer = new Timer();
timer.schedule(new TimerTask() {
    @Override
    public void run() {
        System.out.println("hello");
   }
}, 3000);

实现定时器

定时器的构成:

  • 一个带优先级的阻塞队列(因为阻塞队列中的任务都有各自的执行时间(delay),最先执行的任务一定是delay最小的,使用带优先级队列就可以高效的把这个delay最小的任务找出来)
  • 队列中的每个元素是一个Task对象(Task类用来描述一个任务)
  • 创造一个扫描队首元素的线程,判断队首元素是否需要执行
//使用Task类来描述一个任务
    static class Task implements Comparable<Task>{
        //command 表示这个任务是什么。time使用毫秒级的时间戳表示这个任务什么时候到时间
        private Runnable command;
        private long time;
        //参数time是一个时间差,表示任务执行的时间与此时的时间差。this.time保存一个绝对的时间(毫秒级时间戳,也就是任务执行的具体时间)
        public Task(Runnable command, long time) {
            this.command = command;
            this.time = System.currentTimeMillis()+time;
        }
        public void run(){
            command.run();
        }
		//Task需要实现Comparable接口,重写compareTo方法,来定义优先级队列的比较规则
        @Override
        public int compareTo(Task o) {
            return (int)(this.time-o.time);
        }
    }
    static class Timer {
        //用带有阻塞功能的优先级队列来组织task,队首元素为执行时间最早的任务
        private PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue<>();
        private Object locker = new Object();
        public void schedule(Runnable command,long delay){//参数是一个任务,多长时间执行
            Task task = new Task(command,delay);
            queue.put(task);
            //每次加入一个新任务时,需要唤醒扫描线程,让它能够重新计算wait 的时间,保证新任务不错过
            synchronized (locker){
                locker.notify();
            }
        }
        public Timer(){
            //构造方法内创造一个扫描线程,用来判定当前队首任务是否到时间执行了
            //但是如果执行时间还很早,这个代码就会一直执行循环,出现"忙等",所以要使用wait notify
            //如果使用sleep,一旦插入新的任务且任务比队首元素任务执行时间早,这样的话就会错过这个新任务的执行时间
            Thread t = new Thread(){
                @Override
                public void run() {
                    while (true){
                        try {
                            Task task = queue.take();
                            long curTime = System.currentTimeMillis();
                            if(task.time > curTime){
                            //如果执行时间大于当前时间,表示还没到执行时间,暂时不执行
                            //take会将队首元素删除,但是队首元素的任务还没执行,所以要将task再放回队列
                                queue.put(task);
                                //根据时间差来进行一个等待
                                synchronized (locker){
                                    locker.wait(task.time - curTime);
                                }
                            }else{
                                task.run();
                            }
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            //如果出现interrupt方法,就退出线程
                            break;
                        }
                    }
                }
            };
            t.start();
        }
    }

    public static void main(String[] args) {
        Timer timer = new Timer();
        System.out.println("程序启动");
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello ");
            }
        },3000);
    }

8.4 线程池

线程是为了解决频繁的创建和销毁进程造成的额外开销而引入的,但是在某些场景中,也可能频繁的创建销毁线程,此时创建销毁线程的开销也就无法忽视了,创建和销毁线程会涉及到用户态和内核态之间的切换,为了解决这一问题,从而引入了线程池,线程池类似于字符串常量池,当我们需要使用线程的时候,就直接从线程池中取一个线程出来,不再使用这个线程的时候,就将它又放回线程池,此时这个 *** 作都会在用户态 *** 作,会比创建销毁线程的效率更高。

标准库中的线程池

  • 使用 Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池.
  • 返回值类型为 ExecutorService
  • 通过 ExecutorService.submit 可以注册一个任务到线程池中.
ExecutorService pool = Executors.newFixedThreadPool(10);
for(int i = 0; i < 20; i++){
   	pool.submit(new Runnable() {
    @Override
    public void run() {
        System.out.println("hello");
    }
  }
});
//当线程池里有10个工作线程,而任务队列中加入了20个任务,此时这10个工作线程就会从任务队列中先取出10个任务,然后并发执行这些任务,哪个线程执行完了当前的任务,就会去任务队列中再重新取一个新的任务,直到把线程池任务队列中的任务都取完了,此时线程池的工作线程就会阻塞等待

Excutors创建线程池的几种方式

  • newFixedThreadPool:创建固定线程数的线程池
  • newCachedThreadPool:创建线程数目动态增长的线程池
  • newSingleThreadExcutor:创建只包含单个线程的线程池
  • newScheduledThreadPool:设定延迟时间后执行命令,或者定期执行命令,

Executors本质上是ThreadPoolExecutor类的封装

实现线程池:

  • 核心 *** 作为submit,将任务加入到线程池中
  • 使用Worker类描述一个工作线程,使用Runnable描述一个任务
  • 使用BlockingQueue组织所有的任务
  • 每个worker线程要做的事情:不停的从阻塞队列中取任务并执行
  • 指定线程池中的最大线程数MAX_WORKER_COUNT,当当前线程数超过这个最大线程数时,就不会再新创建线程了
public class ThreadDemo {
    static class Worker extends Thread{
        @Override
        public void run() {
            //工作线程的具体逻辑
            //需要从阻塞队列中取任务
            while (true){
                try {
                    Runnable command = queue.take();
                    command.run();//执行这个具体的任务
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        private BlockingQueue<Runnable> queue = null;
        //构造Worker时就将阻塞队列的实例当作参数传到Worker类中
        public Worker(BlockingQueue<Runnable> queue){
            this.queue = queue;
        }
    }
    static class ThreadPool{
        private static final int MAX_WORKER_COUNT = 10;//线程池的最大线程数目
        private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();//组织任务
        private List<Thread> works = new ArrayList<>();//这个works存放线程池中的工作线程
        //通过submit方法把任务加入到线程池中,可以将任务放入阻塞队列,也可以负责创建线程
        public void submit(Runnable command) throws InterruptedException {
            if(works.size() < MAX_WORKER_COUNT){
             //如果当前工作线程的数量不足线程数目上限,就创建新的线程
             //Worker内部要能取到队列内容,也就是任务,所以需要通过Worker的构造方法,将队列的实例传过去
                Worker worker = new Worker(queue);
                worker.start();
                works.add(worker);
            }
            queue.put(command);
        }
    }
    public static void main(String[] args) throws InterruptedException {
        ThreadPool pool = new ThreadPool();
        for (int i = 0; i < 100; i++) {
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello");
                }
            });
        }
    }
}

9. 保证线程安全的思路
  1. 使用没有共享资源的模型
  2. 使用共享资源只读不写的模型
    1. 不需要写共享资源的模型
    2. 使用不可变的对象
  3. 解决线程安全问题
    1. 保证原子性
    2. 保证顺序性
    3. 保证可见性
10. 对比线程和进程

线程的优点

  1. 创建一个线程的开销要比创建一个进程要小得多
  2. 与进程之间的切换相比,线程之间的切换需要 *** 作系统做的工作要少得多
  3. 线程占用的资源比进程少得多
  4. 能充分利用多处理器的可并行数量
  5. 再等待慢速I/O *** 作结束的同时,程序可以执行其他的任务
  6. 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  7. I/O密集型应用,为了提高性能,将I/O *** 作重叠。线程可以同时等待不同的I/O *** 作。

进程与线程的区别

  1. 进程是系统进行资源分配的最小单位,线程是系统调度的最小单位
  2. 进程有自己的虚拟地址空间,线程只独享指令流执行的必要资源,如寄存器和栈
  3. 由于同一进程的各线程间共享内存和文件资源,可以不通过内核进行直接通信
  4. 线程的创建、切换及终止效率更高。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/720834.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-25
下一篇 2022-04-25

发表评论

登录后才能评论

评论列表(0条)

保存