线程状态与线程安全

一、线程的状态

1.1 所有的线程状态

在Java/JVM中,对于线程的状态做了一个更明确的区分,比系统中的状态还要详细一些

package thread;

public class Demo2 {
    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对象,还没调用start方法,此时系统内核中还没有线程
  • RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作,就绪状态(正在CPU上运行或处在就绪队列中)
  • BLOCKED: 这几个都表示排队等着其他事情
  • WAITING: 这几个都表示排队等着其他事情
  • TIMED_WAITING: 这几个都表示排队等着其他事情
  • TERMINATED: 工作完成了,系统里面的线程已经执行完毕,销毁了(相当于线程的run执行完了),但是Thread对象还在

【代码示例1】:

package thread;

public class Demo3 {
    public static void main(String[] args) {
        Thread t=new Thread(()->{
            while (true){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        System.out.println(t.getState());
        t.start();
    }
}

执行结果:
在这里插入图片描述
【说明】:
在start之前获取的线程状态,创建好了Thread类对象,但没调用start

【代码示例2】:

package thread;

public class Demo3 {
    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(()->{
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
        });
        //在start之前获取,获取到的是线程还未创建的状态
        System.out.println(t.getState());
        t.start();
        System.out.println(t.getState());
        t.join();
        //在join之后获取,获取到的是线程已经结束后的状态
        System.out.println(t.getState());
    }
}

//执行结果:
NEW
RUNNABLE
hello thread
TERMINATED

【代码示例3】:

package thread;

public class Demo3 {
    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(()->{
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
        });
        //在start之前获取,获取到的是线程还未创建的状态
        System.out.println(t.getState());
        t.start();
        Thread.sleep(500);
        //此时t线程大概率处在阻塞状态,因为在main线程中只sleep了500ms
        //而在t线程中sleep了1000ms
        System.out.println(t.getState());
        t.join();
        //在join之后获取,获取到的是线程已经结束后的状态
        System.out.println(t.getState());
    }
}
//执行结果:
NEW
hello thread
TIMED_WAITING
TERMINATED

1.2 线程的状态转换

在这里插入图片描述
简化图:
在这里插入图片描述
主干道是NEW->RUNNBALE->TERMINATED
在RUNNABLE会根据特定代码进入支线任务,这些支线任务都是"阻塞状态",进入的方式不一样,阻塞的时间就不同,唤醒的方式也不同

【补充】:
yield() 让调用者暂时放弃CPU,重新在就绪队列中排队相当于sleep(0);yield 不改变线程的状态, 但是会重新去排队.

二、线程安全(重点)

线程安全问题的万恶之源,正式调度器随机调度/抢占式执行
线程不安全:通俗来讲就是,在随机调度之下,执行程序的多种可能,其中的某些可能会导致代码出现bug,这就叫线程不安全/线程安全问题

2.1 线程不安全典型示例

2.1.1 多个线程修改同一变量

public class Demo3 {

   static class Counter{
       public  int count=0;
       public void increase(){
           count++;
       }
   }
  //创建两个线程,使这两个线程同时并发对同一个变量,自增5w次
    //预期最终能将变量自增10w次
    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("count:"+counter.count);
   }

}

对于这个代码的执行结果来说是不确定的,大概率在50000~100000之间,这也就是我们所说的线程不安全的问题,随机调度顺序不同,就导致程序的运行结果不同

【bug分析】:此时我们需要站在硬件的角度来理解代码执行的过程,执行代码时,CPU会把对应指令从内存中读取出来,然后执行(CPU自身也包含了一些寄存器,这些寄存器也能存储少量的数据)
像上面的count++,这一条执行实际对应的是3条指令

1)从内存读取数据到CPU ,load操作
2)在CPU的寄存器中完成加法运算,add操作
3)把寄存器的数据写回到内存中,save操作

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在极端情况下,如果所有的指令排列方式都是上面这两种等价于串行的排列方式,count的结果就是100000,,如果所有的指令排列中,都没有等价于串行排列的这两种,那么count结果就是50000,实际情况种调度的方式是不确定的,
但最终结果一定在50000-100000

【注意】:
但也有的时候出现小于50000的情况,至于这种情况目前我还不能解释清楚
在这里插入图片描述

2.1.2 内存可见性

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

package thread;

import java.util.Scanner;

public class Test3 {
    static  class  Counter{
      public int flag=0;
    }

    public static void main(String[] args) {
        Counter counter=new Counter();
         Thread t1=new Thread(()->{
             while (counter.flag==0){

             }
             System.out.println("t1 结束");
         });
         t1.start();
         Thread t2=new Thread(()->{
             //让用户输入一个整数,赋值给flag
             Scanner scanner=new Scanner(System.in);
             System.out.println("请输入一个整数:");
             counter.flag=scanner.nextInt();
         });
         t2.start();
    }
}

预期的执行结果是:当用户输入一个整数后,t1线程结束,整个进程结束
实际运行结果:
在这里插入图片描述
【bug分析】:
在这里插入图片描述
此处的优化操作,在单线程的环境下是没问题的,但在多线程的环境下就可能有问题,因为多线程的环境太复杂,编译器/JVM/操作系统在进行优化的时候可能产生误判,针对这个问题,java引入了volatile关键字,让程序员手动的禁止编译器对某个变量进行上述的优化

2.1.3 指令重排序

指令重排序,也是JVM/操作系统/编译器的优化操作
为什么有各种各样的优化操作呢?
因为实现JVM/操作系统的大佬,不信任程序员,即使程序员写出的代码比较拉跨,也没关系,因为经过优化操作之后会让代码变得更高效
一段代码的执行:

  1. 去前台取下U盘
  2. 去教室写10 分钟教师
  3. 去前台取快递

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

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

Test t=new Test();
//这段代码在底层实际上由三条指令完成
//1. 创建内存空间
//2.在内存空间上构造一个对象
//3.将内存空间的引用赋值给t

这段代码的三条指令在底层可能会发生重排序1->3->2,在单线程下发生重排序后没有影响,但是在多线程的执行下可能会出现线程不安全
例如,同时还有新线程尝试读取t的引用,
如果指令按照1->2->3的顺序执行,当新线程读到t为非null的时候,此时t一定是一个有效的对象
如果指令按照1->3->2的顺序执行,当新线程读到t为非null的时候,此时t可能是一个无效的对象

【小结】:造成线程不安全的原因

  1. 操作系统的随机调度/抢占式执行
  2. 多个线程修改同一个变量会造成线程不安全,如果只是一个线程修改变量、多个线程读同一个变量,多个线程修改不同的变量不会造成线程不安全
    上面这种示例就属于多个线程修改同一个线程变量
  3. 有些修改操作不是原子(不可拆分的最小单位)操作
    通过=来修改,=(赋值)只对应一条机器指令,这种操作就是原子操作
    通过++/–来修改,++/–都对应三条机器指令,这样的操作就不是原子操作
  4. 内存可见性引起的线程安全问题,这属于是一个线程写,一个线程读的场景
  5. 指令重排序

2.2 解决线程不安全

在上面的多线程修改同一变量的实例中,出现线程不安全的根本原因是自增操作不是原子操作,由load、add、save三条机器指令构成,通过加锁能将这三条指令打包成一个原子操作

加锁过程示例:
在这里插入图片描述

2.2.1 synchronized 关键字(监视器锁monitor lock)

synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待

  • 进入 synchronized 修饰的代码块, 相当于 加锁
  • 退出 synchronized 修饰的代码块, 相当于 解锁

synchronized使用示例:

package thread;

public class Test1 {
    static class Counter{
        public  int count=0;
        public synchronized void increase(){
            count++;
        }
    }
    //创建两个线程,使这两个线程同时并发对同一个变量,自增5w次
    //预期最终能将变量自增10w次
    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("count:"+counter.count);
    }
}
//执行结果:
count:100000 

在这里插入图片描述
synchronized用的锁是存在Java对象头里的
在这里插入图片描述

可以粗略理解成, 每个对象在内存中存储的时候, 都存有一块内存表示当前的 “锁定” 状态(类似于厕所的"有人/无人").
如果当前是 “无人” 状态, 那么就可以使用, 使用时需要设为 “有人” 状态.
如果当前是 “有人” 状态, 那么其他人无法使用, 只能排队

【理解阻塞等待】:

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

【注意】:

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

2.2.2 synchronized的使用

synchronized 本质上要修改指定对象的 “对象头”. 从使用角度来看, synchronized 也势必要搭配一个具体的对象来使用.
① 直接修饰普通方法:锁的是Counter对象

 static class Counter{

 public synchronized void func3(){

        }
}

② 修饰静态方法: 锁的是Counter类对象

  static class Counter{
        public static synchronized void func2(){

        }
  }

③修饰代码块:
锁当前对象

static class Counter{
public void func4(){
            synchronized (this){

            }
        }
}        
        

锁类对象(类对象在整个程序中是唯一的)

static class Counter{
 public static void func1(){
            synchronized (Counter.class){

            }
        }
}

【说明】:

  • func1和func2加锁的方式是等价的,都是对类对象进行加锁,当一个线程调用func1方法,另一个线程调用func2方法,两个线程之间会发生竞争,因为这两个线程都要对同一个类对象加锁

  • func3和func4加锁的方式是等价的,都是对this对象进行加锁,但this对象是有多个的

    Counter counter1=new Counter();
    counter1.func4();
    Counter counter2=new Counter();
    counter2.func4(); 
    

这段代码不会发生锁竞争,因为这两个func4中的this是不同的一个是counter1,一个是counter2

【注意】:

  • 在Java中,任何一个对象都可以作为锁对象(都可以放在synchronized后面的括号中)
  • 当多个线程想要获取同一把锁的时候,同一时刻只有一个线程能获取到这把锁,其他线程阻塞等待
  • 多个线程尝试dui不同的对象加锁, 相互之间不会出现互斥的情况
  • 锁竞争:因为想加的锁被别人获取到了,而产生的阻塞等待

【不发生锁竞争的代码示例】

package thread;

public class Test2 {
    public static Object locker1=new Object();
    public static Object locker2=new Object();

    public static void main(String[] args) {
        Thread t1=new Thread(()->{
           synchronized (locker1) {
               System.out.println("t1 start");
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               System.out.println("t1 end");
           }
        });
        Thread t2=new Thread(()->{
            synchronized (locker2){
                System.out.println("t2 start");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t2 end");
            }
        });
        t1.start();
        t2.start();
    }
}
//执行结果
t1 start
t2 start
t1 end
t2 end
//在t1 end前,t2就start了,说明t1和t2未发生锁竞争

【发生锁竞争的代码示例】

package thread;

public class Test2 {
    public static Object locker1=new Object();
    public static Object locker2=new Object();

    public static void main(String[] args) {
        Thread t1=new Thread(()->{
           synchronized (locker1) {
               System.out.println("t1 start");
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               System.out.println("t1 end");
           }
        });
        Thread t2=new Thread(()->{
            synchronized (locker1){
                System.out.println("t2 start");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t2 end");
            }
        });
        t1.start();
        t2.start();
    }
}
//执行结果:
t1 start
t1 end
t2 start
t2 end
//在t1 end之后,t2才start,此时t1和t2因竞争locker发生锁竞争

2.2.3 volatile 关键字

volatile 修饰的变量, 能够保证 “内存可见性”.
在这里插入图片描述
代码在写入 volatile 修饰的变量的时候,

  • 改变线程工作内存中volatile变量副本的值
  • 将改变后的副本的值从工作内存刷新到主内存

代码在读取 volatile 修饰的变量的时候,

  • 从主内存中读取volatile变量的最新值到线程的工作内存中
  • 从工作内存中读取volatile变量的副本

上述过程,在Java中叫做JMM,Java Memory Model

Java为什么要弄一个专门的名词来表示这个过程呢?

跨平台,对程序员屏蔽硬件差异,因为实际的计算机中,硬件的差异很大,例如早期的CPU没有缓存,后来逐渐有了L1,L2,L3三级缓存,这些结构的不同都会对上述的"内存可见性"的描述过程产生影响,所以Java就将这些CPU缓存或CPU寄存器都统称为工作内存

volatile,强制读写内存,速度慢了,但数据更准确了

volatile 不保证原子性
volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性, volatile 保证的是内存可见性
volatile能禁止指令重排序

2.2.4 volatile 使用示例

【volatile保证内存可见性代码示例】

package thread;

import java.util.Scanner;

public class Test3 {
    static  class  Counter{
      public volatile int flag=0;
    }

    public static void main(String[] args) {
        Counter counter=new Counter();
         Thread t1=new Thread(()->{
             while (counter.flag==0){

             }
             System.out.println("t1 结束");
         });
         t1.start();
         Thread t2=new Thread(()->{
             //让用户输入一个整数,赋值给flag
             Scanner scanner=new Scanner(System.in);
             System.out.println("请输入一个整数:");
             counter.flag=scanner.nextInt();
         });
         t2.start();
    }
}
//执行结果:
请输入一个整数:
88
t1 结束

【在while循环中加sleep能避免优化】:

package thread;

import java.util.Scanner;

public class Test3 {
    static  class  Counter{
      public  int flag=0;
    }

    public static void main(String[] args) {
        Counter counter=new Counter();
         Thread t1=new Thread(()->{
             while (counter.flag==0){
                 try {
                     Thread.sleep(1000);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             }
             System.out.println("t1 结束");
         });
         t1.start();
         Thread t2=new Thread(()->{
             //让用户输入一个整数,赋值给flag
             Scanner scanner=new Scanner(System.in);
             System.out.println("请输入一个整数:");
             counter.flag=scanner.nextInt();
         });
         t2.start();
    }
}
//执行结果:
请输入一个整数:
99
t1 结束

【分析】:

此时我们并没有在变量前加volatile修饰,而是在while循环中使用sleep,也避免了编译器的优化。
这是因为,编译器的优化是根据代码的实际情况来进行的,在while的循环体中为空时,循环转速极快,导致了读内存的操作非常频繁,所以触发了优化
而在while的循环体中加入了sleep之后,读内存的操作不是很频繁了,就没有触发优化

【volatile不保证原子性代码示例】

package thread;

import java.util.Hashtable;

public class Test1 {
    static class Counter{

     volatile  public int count=0;
     void increase(){
         this.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);
    }
}
//执行结果:
52174

2.3 wait和notify

由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知.
但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序
在这里插入图片描述
球场上的每个运动员都是独立的 “执行流” , 可以认为是一个 “线程”.
而完成一个具体的进攻得分动作, 则需要多个运动员相互配合, 按照一定的顺序执行一定的动作, 线 程1 先 “传球” , 线程2 才能 “扣篮”

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

【注意】:wait, notify, notifyAll 都是 Object 类的方法.

2.3.1 wait()方法

wait方法的内部执行过程:

  1. 释放锁(这也就意味着,在调用wait之前要先拿到锁,所以wait一定要放在synchronized中使用,而且synchronized中加锁的对象和调用wait方法的对象是同一个对象)
  2. 等待通知
  3. 当通知到达之后,就会被唤醒,并且尝试重新获取锁

wait 结束等待的条件:

  • 其他线程调用该对象的 notify 方法.

  • wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).在这里插入图片描述

  • 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常

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

【错误示范】:

package wait;

public class Test1 {
    public static void main(String[] args) throws InterruptedException {
        Object object=new Object();
        System.out.println("wait 前");
        object.wait();
        System.out.println("wait 后");
    }
}

执行结果:
在这里插入图片描述
【正确示例】:

package wait;

public class Test1 {
    public static void main(String[] args) throws InterruptedException {
        Object object=new Object();
        synchronized (object){
            System.out.println("wait 前");
            object.wait();
            System.out.println("wait 后");
        }
    }
}

执行结果:
在这里插入图片描述
在这里插入图片描述

2.3.2 notify()方法

notify 方法是唤醒等待的线程.

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

【使用示例】:

package wait;

import java.util.Scanner;

public class Test1 {
    public static Object object=new Object();
    public static void main(String[] args) throws InterruptedException {
    //等待的线程
        Thread waitTask=new Thread(()->{
        synchronized (object){
            System.out.println("wait 开始");
            try {
                object.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("wait 结束");
        }
    });
    waitTask.start();
    //用来通知/唤醒的线程
    Thread notifyTask=new Thread(()->{
        System.out.println("请输入任意内容,开始通知:");
        Scanner scanner=new Scanner(System.in);
        scanner.next();//阻塞等待,直到用户输入内容
      synchronized (object) {
        System.out.println("notify 开始");
        object.notify();
        System.out.println("notify 结束");
    }
    });
    notifyTask.start();
    }
}

执行结果:

在这里插入图片描述
【错误使用示例】:wait等待的是一个对象,notify唤醒的是另一个对象,则无作用

package wait;

import java.util.Scanner;

public class Test1 {

    public static Object locker1=new Object();
    public static Object locker2=new Object();

    public static void main(String[] args) throws InterruptedException {
    //等待的线程
        Thread waitTask=new Thread(()->{
        synchronized (locker1){
            System.out.println("wait 开始");
            try {
                locker1.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("wait 结束");
        }
    });
    waitTask.start();
    //用来通知/唤醒的线程
    Thread notifyTask=new Thread(()->{
        System.out.println("请输入任意内容,开始通知:");
        Scanner scanner=new Scanner(System.in);
        scanner.next();//阻塞等待,直到用户输入内容
      synchronized (locker2) {
        System.out.println("notify 开始");
        locker2.notify();
        System.out.println("notify 结束");
    }
    });
    notifyTask.start();
    }
}

执行结果:
在这里插入图片描述

【补充】:

  • wait()如果没有别唤醒,就会"死等"
  • 在调用notify方法的时候,会尝试进行通知,如果当前对象没有在其他线程里wait,也不会有副作用

wait和notify机制还能有效的避免"线程饿死",有些情况下,调度去分配的不均匀,导致有些线程反复占用CPU,有些线程始终得不到CPU
在这里插入图片描述

2.3.3 notifyAll()方法

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

【notify唤醒3个线程中的一个】

package wait;

import java.util.Scanner;

public class Test1 {

    public static Object locker1=new Object();

    public static void main(String[] args) throws InterruptedException {
    //等待的线程
        Thread waitTask1=new Thread(()->{
        synchronized (locker1){
            System.out.println("wait1 开始");
            try {
                locker1.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("wait1 结束");
        }
    });
    waitTask1.start();
        Thread waitTask2=new Thread(()->{
            synchronized (locker1){
                System.out.println("wait2 开始");
                try {
                    locker1.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("wait2 结束");
            }
        });
        waitTask2.start();
        Thread waitTask3=new Thread(()->{
            synchronized (locker1){
                System.out.println("wait3 开始");
                try {
                    locker1.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("wait3 结束");
            }
        });
        waitTask3.start();
    //用来通知/唤醒的线程
    Thread notifyTask=new Thread(()->{
        System.out.println("请输入任意内容,开始通知:");
        Scanner scanner=new Scanner(System.in);
        scanner.next();//阻塞等待,直到用户输入内容
      synchronized (locker1) {
        System.out.println("notify 开始");
        locker1.notify();
        System.out.println("notify 结束");
    }
    });
    notifyTask.start();
    }
}
//执行结果:
wait1 开始
wait2 开始
wait3 开始
请输入任意内容,开始通知:
678
notify 开始
notify 结束
wait1 结束

【notifyAll同时唤醒3个线程】

package wait;

import java.util.Scanner;

public class Test1 {

    public static Object locker1=new Object();

    public static void main(String[] args) throws InterruptedException {
    //等待的线程
        Thread waitTask1=new Thread(()->{
        synchronized (locker1){
            System.out.println("wait1 开始");
            try {
                locker1.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("wait1 结束");
        }
    });
    waitTask1.start();
        Thread waitTask2=new Thread(()->{
            synchronized (locker1){
                System.out.println("wait2 开始");
                try {
                    locker1.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("wait2 结束");
            }
        });
        waitTask2.start();
        Thread waitTask3=new Thread(()->{
            synchronized (locker1){
                System.out.println("wait3 开始");
                try {
                    locker1.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("wait3 结束");
            }
        });
        waitTask3.start();
    //用来通知/唤醒的线程
    Thread notifyTask=new Thread(()->{
        System.out.println("请输入任意内容,开始通知:");
        Scanner scanner=new Scanner(System.in);
        scanner.next();//阻塞等待,直到用户输入内容
      synchronized (locker1) {
        System.out.println("notify 开始");
        locker1.notifyAll();
        System.out.println("notify 结束");
    }
    });
    notifyTask.start();
    }
}
//执行结果:
wait1 开始
wait2 开始
wait3 开始
请输入任意内容,开始通知:
8888
notify 开始
notify 结束
wait3 结束
wait2 结束
wait1 结束

理解notify和notifyAll
notify只唤醒等待队列中的一个线程,其他线程继续等待
在这里插入图片描述
notifyAll一下全部唤醒,需要这些线程重新获取锁
在这里插入图片描述
【注意】:
虽然是同时唤醒 3 个线程, 但是这 3 个线程需要竞争锁. 所以并不是同时执行, 而仍然是有先有后的执行.

wait和sleep的对比(面试题)

共同点:

  • 都是使线程暂停一段时间的方法

不同点:

  • wait是Object类中的一个方法,sleep是Thread类中的一个方法;
  • wait必须在synchronized修饰的代码块或方法中使用,sleep方法可以在任何位置使用;
  • wait被调用后当前线程进入BLOCK状态并释放锁,并可以通过notify和notifyAll方法进行唤醒;sleep被调用后当前线程进入TIMED_WAIT状态,不涉及锁相关的操作;

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

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

  • ArrayList
  • LinkedList
  • HashMap
  • TreeMap
  • HashSet
  • TreeSet
  • StringBuilder

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

  • Vector (不推荐使用)
  • HashTable (不推荐使用,把所有的关键方法都无脑的加了synchronized修饰,加锁的代价,未获取到锁的线程会阻塞等待,会牺牲很大的运行速度)
  • ConcurrentHashMap(线程安全的哈希表,内部做了一系列的优化手段,来提高效率,推荐使用)
  • StringBuffer(StringBuffer 的核心方法都带有 synchronized )

还有的虽然没有加锁, 但是不涉及 “修改”, 仍然是线程安全的

  • String

版权声明:本文为m0_60631323原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。