【JavaEE】线程安全问题

目录

一.线程安全问题

1.什么是线程安全

2.线程不安全的原因

3.如何解决线程安全问题?

3.1synchronized的使用方式

3.2解决示例自增带来的线程安全问题

(1)对代码块进行加锁

 (2)对方法进行加锁

4.synchronized的特性

5.死锁

5.1两个线程两把锁

5.2N个线程M把锁

​编辑 5.3造成死锁的必要条件

5.4如何避免出现死锁?

6.volatile关键字

7.wait和notify

7.1wait

7.2notify

notify和notifyAll的区别 

小练习

 7.3wait和sleep的区别


 在上一篇中,我们讲解了线程以及如何在java中创建线程,但在多线程之间存在着线程安全问题,本篇我们就围绕线程安全问题来展开。

一.线程安全问题

1.什么是线程安全

线程安全是指在多线程环境下,共享数据的访问和操作不会引起不正确的结果。具体来说,线程安全的程序能够正确地处理多个线程同时访问共享数据的情况,保证数据的一致性和正确性。当两个或者多个线程同时访问共享的数据时,导致数据不一致,称为线程安全问题

2.线程不安全的原因

  1. 线程在操作系统中是随机调度、抢占式执行的
  2. 多个线程同时修改同一个变量
  3. 修改操作不具"原子性"
  4. 内存可见性:一个线程对共享变量值的修改,能够及时被其他线程看到。
  5. 指令重排序:计算机系统在执行程序时,为了提高程序性能,可能会对指令进行重新排序的操作。

示例:我们这里来利用两个线程来让count累加,每个线程体中循环次数为5w次。如下:

package Threads.threadtext;

/**
 * Demo 类用于演示线程安全问题。
 * 本类中,两个线程同时增加一个静态变量 count 的值,以展示并发情况下可能出现的不一致问题。
 */
public class Demo {
    // count 用于演示线程安全问题,初始值为 0。
    public static int count=0;//静态变量

    /**
     * 程序入口。
     * 创建两个线程,每个线程执行 50000 次 count 的增加操作。
     * 随后等待两个线程执行完毕,并打印最终的 count 值。
     * 期望输出为 100000,但实际运行结果可能因并发问题而小于该值。
     *
     * @param args 命令行参数
     * @throws InterruptedException 如果线程在等待时被中断
     */
    public static void main(String[] args) throws InterruptedException{
        // 创建线程 t1,负责增加 count 变量的值
        Thread t1=new Thread(()->{
            for(int i=0;i<50000;i++){
                count++;
            }
        });

        // 创建线程 t2,同样负责增加 count 变量的值
        Thread t2=new Thread(()->{
            for(int i=0;i<50000;i++){
                count++;
            }
        });

        // 启动线程 t1 和 t2
        t1.start();
        t2.start();

        // 等待线程 t1 和 t2 执行完毕
        t1.join();
        t2.join();

        // 打印最终的 count 值
        System.out.println(count);
    }
}

在上面的代码中,我们期望的count的是10w,但实际真的有10w吗?我们可以运行一下。

我们可以看到最后count的值为89931,这与我们期望的值相差挺大。但其实我们在每次运行之后,显示的答案都会不同,这主要是与线程在cpu中是抢占式执行、随机调度有关

在上图中,要执行count++过程,从cpu上要分为三个指令

指令是cpu执行的最基本单位,线程要调度,也至少需要将当前的指令执行完):

  1. 在内存中读取数据到cpu寄存器里(load)
  2. 把cpu寄存器里的数据+1              (add)
  3. 把寄存器里的数据写回到内存中  (save)

 

由于count++是三条指令,且线程在cpu中是抢占式执行、随机调度的,所以可能会出现cpu刚执行一条指令或者2个指令、3个指令被调走的情况。基于这些情况,当两个线程同时进行对count++时,就会出现线程安全问题。

以下是根据时间轴来画指令的执行情况:

若按照以上这种调度顺序执行,则可以得到10w,但这个概念非常小,由于是随机调度的,因此在计算时会产生很多其他的执行顺序,以下是例举的一些执行顺序:

在上图中,这几种执行顺序最终都不能让count的值为10w。

3.如何解决线程安全问题?

我们可以用synchronized关键字来对代码块或方法进行加锁。

3.1synchronized的使用方式

1)对指定代码块进行加锁

    synchronized(锁对象){
        代码块
    }

2)对指定方法进行加锁

    synchronized 权限修饰符 (static) 返回值 方法名(参数){
        代码块
    }

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

注意:

  • 若synchronized修饰的是一个静态的方法,就相当于针对当前类对象进行加锁。
  • 若synchronized修饰的是一个普通的方法,就相当于针对this进行加锁。

锁对象的作用:锁对象可以是任意的Object/Object子类的对象,锁对象是谁并不重要,重要的是两个或多个线程的锁对象是否是同一个。若是同一个,则会出现锁竞争/锁冲突。反之,若不是针对同一个锁对象加锁,则不会出现。 

使用锁,本质上就是将线程从并行执行-->串行执行,这样来解决线程安全问题。

3.2解决示例自增带来的线程安全问题

(1)对代码块进行加锁

对上述的示例进行加锁,这里我们实例一个Object类对象locker作为锁对象,即:


/**
 * Demo 类用于演示线程安全问题。
 * 本类中,两个线程同时增加一个静态变量 count 的值,以展示并发情况下可能出现的不一致问题。
 */
public class Demo {
    // count 用于演示线程安全问题,初始值为 0。
    public static int count = 0;//静态变量
    static Object locker = new Object();//锁对象

    /**
     * 程序入口主方法。
     * 创建两个线程,每个线程循环50000次,对静态变量count进行自增操作。
     * 使用synchronized关键字确保在同一时间只有一个线程可以访问count变量,以演示线程安全。
     *
     * @param args 命令行参数
     * @throws InterruptedException 如果线程在等待、通知或唤醒过程中被中断
     */
    public static void main(String[] args) throws InterruptedException {
        // 创建线程 t1,负责增加 count 变量的值
        Thread t1 = new Thread(()->{
            for(int i=0;i<50000;i++){
                synchronized (locker){
                    count++;
                }
            }
        });

        // 创建线程 t2,同样负责增加 count 变量的值
        Thread t2 = new Thread(()->{
            for(int i=0;i<50000;i++){
                synchronized (locker){
                    count++;
                }
            }
        });

        // 启动线程 t1 和 t2
        t1.start();
        t2.start();

        // 等待线程 t1 和 t2 执行完毕
        t1.join();
        t2.join();

        // 打印最终的 count 值
        System.out.println(count);
    }
}

可以看到,在加锁之后,count的值能达到我们所期望的。

 (2)对方法进行加锁
class Counter {
    public int count = 0;
    synchronized public void add(){
        count++;
    }
}

public class Demo1 {

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        // 创建线程 t1,负责增加 count 变量的值
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                synchronized (counter) {
                    counter.add();
                }
            }
        });

        // 创建线程 t2,同样负责增加 count 变量的值
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                synchronized (counter) {
                    counter.add();
                }
            }
        });

        // 启动线程 t1 和 t2
        t1.start();
        t2.start();

        // 等待线程 t1 和 t2 执行完毕
        t1.join();
        t2.join();

        // 打印最终的 count 值
        System.out.println(counter.count);
    }
}

通过对类的实例对象的方法进行加锁<==>对类的实例对象进行加锁,也可以写成:

class Counter {
    public int count = 0;
    public void add(){
        synchronized (this){
            count++;
        }
    }
}

对静态方法进行加锁<==>对该类的类对象进行加锁


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

public class Demo1 {

    public static void main(String[] args) throws InterruptedException {
        // 创建线程 t1,负责增加 count 变量的值
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                synchronized (Counter.class) {
                    Counter.add();
                }
            }
        });

        // 创建线程 t2,同样负责增加 count 变量的值
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                synchronized (Counter.class) {
                    Counter.add();
                }
            }
        });

        // 启动线程 t1 和 t2
        t1.start();
        t2.start();

        // 等待线程 t1 和 t2 执行完毕
        t1.join();
        t2.join();

        // 打印最终的 count 值
        System.out.println(Counter.count);
    }
}
class Counter {
    public static int count = 0;
    public static void add(){
        synchronized (Counter.class){
            count++;
        }
    }
}

为什么说是对该类的类对象进行加锁?

静态方法在java中是不需要创建类的实例对象就能进行调用。可以由该类本身或者该类的对象的引用来引用。

类对象:一个类只有一个类对象;

一个类对象中包含:

  1. 类有哪些属性,都是啥名字,啥类型,权限
  2. 类的方法有哪些,都是啥名字,参数,类型,权限
  3. 类自身继承了哪个类,实现了哪些接口等等

4.synchronized的特性

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

假设现在有三个人准备上厕所,但是厕所只有一个,那么当第一个人进去厕所后,其余的人要等待前面的人上完厕所才能上。即一个线程先上了锁,其他线程只能等待这个线程释放,这个等待的过程称为“阻塞等待”。

2.可重入synchronized同步块对同一条线程来说是可重入的,不会出现死锁的情况。

 我们来看个例子:

class Demo1 {
    static Object locker = new Object();
    public static void main(String[] args) throws InterruptedException {
        
        Thread t1 = new Thread(()->{
            synchronized (locker){
                synchronized (locker){
                    System.out.println("t1");
                }
            }
        });
        t1.start();
    }
}

 在这个例子中,线程t1首先会对locker对象进行加锁,但是在给locker加完一次之后,再加一次锁,就会失败,为什么?第二次加锁需要第一次加锁释放之后才能加锁,但第一次加锁释放需要执行完synchronized修饰的代码块,很遗憾的是,由于第二次加锁在发现对象已经有锁之后,会进行阻塞等待状态,直到第一个锁被释放后才能进行加锁。从而造成了“死锁”。

但在java中,synchronized锁具有可重入性,即:在同一线程中对同一个锁对象加多次锁,不会造成死锁。

那如果在一个线程中,对同个锁对象加多次锁,什么时候才能释放?

可重入锁的内部,包含了:“线程持有者”和“计数器”。

在一个线程,如果某个线程加锁的时候,发现锁已经被占用,且占用的恰好是自己,那么仍然可以继续获取到锁,并让计数器自增。解锁时计数器递减为0时,才能真正释放锁。

5.死锁

如果没有snychronized的可重入性,那么如果针对一个锁连续加多次锁,就会出现死锁的情况。

常见的死锁有两种:

  1. 两个线程两把锁
  2. N个线程M把锁

5.1两个线程两把锁

现有两个线程t1和t2,线程t1已经获得了锁1,线程2已经获得了锁2,同时,线程t1想要获得锁2,线程t2想要获得锁1,就会发生死锁。

class Demo2{
    //锁对象
    private static Object locker1=new Object();
    private static Object locker2=new Object();
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            synchronized (locker1) {
                System.out.println("t1获得锁1");
                //让线程睡眠1秒,是为了让两个线程都能先拿到锁,如果没有sleep,执行结果就不可控,可能就会出现某个线程一次拿了两个锁,另一个线程还在执行,无法构成死锁
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (locker2){
                    System.out.println("t1获得锁2");
                }
            }
        });

        Thread t2=new Thread(()->{
            synchronized (locker2) {
                System.out.println("t2获得锁2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (locker1){
                    System.out.println("t2获得锁1");
                }
            }

        });
        t1.start();
        t2.start();
    }
}

上述代码在运行之后,会出现两个线程一直僵持的状态

也可以打开jconsole进行查看两个线程的状态,可以看到两个线程都是BLOCKED状态

 

5.2N个线程M把锁

这就得提到一个经典的问题:哲学家进餐问题 。

有5个哲学家共用同一张桌子,分别坐在周围的五张椅子上,在桌子上有5只碗和5根筷子,他们只有两个行为:思考和进餐,在思考的时候,不需要任何动作,在进餐的时候需要分别拿起左右最近的筷子进餐。而且哲学家非常的固执,如果他拿到了筷子,如果没吃到东西是不会放下筷子的。

这些哲学家进餐时间是不定的,如果一个哲学拿到左手边的筷子,此时需要右手边的筷子,但右边的筷子也被另一位哲学家拿了,那么他们就会陷入僵持状态,谁也不让谁。若当5个哲学家同时拿起了筷子,那么就会造成5个哲学家一直吃不到东西。

那么如何解决上述问题,让每个哲学家都能进餐呢? 

我们给哲学家进行编号,从第一个哲学家开始,先拿起左边的筷子,再看看右边的筷子是否有使用,若没有就拿起来。同理,往后的每个哲学家都是先拿起左边的筷子,右边的筷子若有哲学家使用就进行等待。

当1号哲学家用完之后,此时2号哲学家就能进行就餐,以此类推,当4号哲学家进完餐之后,5号就可以拿起他左右边的筷子进行就餐。

 5.3造成死锁的必要条件

  1. 锁是互斥的(锁的基本特性)。当一个线程1被上锁之后,另一个线程2想要上锁,就会进入阻塞状态,等到线程1释放锁。
  2. 锁是不可被抢占的(锁的基本特性)。线程1拿到了锁A,如果线程1不主动释放锁A,线程2就不能把锁A抢过去。
  3. 请求和保持(代码结构)。一个已经有锁线程请求获取另一个锁,但同时又不释放现有的锁。
  4. 循环等待/环路等待/循环依赖(代码结构)。如小白正在玩电脑,同时也想玩手机,而小黑正在玩手机,同时也想玩电脑。但两个人谁也不让谁,都想同时玩电脑和手机,就陷入了僵局。

5.4如何避免出现死锁?

互斥和不可抢占是锁的基本特性,我们可以通过避免代码结构“嵌套锁”,但在某些场景下需要用到。所以最好避免死锁的是破坏循环等待。在加多把锁的时候,先加编号小的锁,再加编号大的锁,且所有的线程都要遵循这一个规则。

class Demo2{
    //锁对象
    private static Object locker1=new Object();
    private static Object locker2=new Object();
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            synchronized (locker1) {
                System.out.println("t1获得锁1");
                //让线程睡眠1秒,是为了让两个线程都能先拿到锁,如果没有sleep,执行结果就不可控,可能就会出现某个线程一次拿了两个锁,另一个线程还在执行,无法构成死锁
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (locker2){
                    System.out.println("t1获得锁2");
                }
            }
        });

        Thread t2=new Thread(()->{
            synchronized (locker1) {
                System.out.println("t2获得锁1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (locker2){
                    System.out.println("t2获得锁2");
                }
            }

        });
        t1.start();
        t2.start();
    }
}

6.volatile关键字

volatile关键字修饰的变量,保证了“内存的可见性”,在编译器判断是否要进行优化时,通过volatile能够让编译器知道当前的变量不需要进行优化。

示例:

class Demo12{
    static int i=0;
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            System.out.println("t1 线程开始");
            while(i==0){
                ;
            }
            System.out.println("t1 线程结束");
        });
        Thread t2=new Thread(()->{
            System.out.println("请输入i的值");
            Scanner scanner=new Scanner(System.in);
            i=scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

当我们在输入一个值后,发现程序并没有因此停止

 

这是为什么呢?

我们都知道变量是存储在内存中的,当我们需要使用这个变量的时候,cpu寄存器会从内存中将该变量的数据读取过来,再将cpu寄存器获取到的变量的值读取到我们的代码中,但是从内存中读取变量的速度远小于直接从寄存器中读取变量的速度。当需要对某个变量进行多次的读取时,编译器会自动优化:即从内存中读取到一次的值存放到cpu寄存器中,剩下的读取操作则直接从寄存器中读取即可。

因此,在这里,当我们启动线程t1,t2时,t1内部的循环可能已经进行成千上万次甚至更多,那么此时编译器就会对其进行优化,直接从cpu寄存器中读取,以此来提高运行速度。所有,当我们在线程t2输入一个值后,线程t1中的变量并不能获取到内存中已经修改的值,就会一直执行下去。

 这就是“内存不可见性”,当在一个线程中对某个变量进行修改,由于编译器的优化,在另外的线程中不能获取到内存中已修改的值。

因此,我们需要使用volatile关键字来修饰变量,让编译器知道这个变量不能进行优化,每次操作都需要从内存中获取到值,而不是直接从cpu寄存器中获取。

/**
 * Demo12 类用于演示两个线程之间的交互。
 * 其中一个线程等待另一个线程输入并设置共享变量 i 的值。
 */
class Demo12{
    /**
     * 共享变量 i,使用 volatile 关键字修饰以确保多线程环境下的可见性。
     */
    volatile static int i=0;
    
    /**
     * 程序入口点。
     * 创建两个线程,一个负责等待直到 i 被赋值,另一个负责从用户输入中获取 i 的值。
     * @param args 命令行参数
     */
    public static void main(String[] args) {
        // 创建线程 t1,负责等待直到 i 的值被设置
        Thread t1=new Thread(()->{
            System.out.println("t1 线程开始");
            while(i==0){
                // 通过空循环等待 i 的值被设置
            }
            System.out.println("t1 线程结束");
        });
        // 创建线程 t2,负责从用户输入中获取 i 的值
        Thread t2=new Thread(()->{
            System.out.println("请输入i的值");
            Scanner scanner=new Scanner(System.in);
            i=scanner.nextInt();
        });
        // 启动两个线程
        t1.start();
        t2.start();
    }
}

虽然使用volatile关键字能够保证线程之间内存是可见的,但是不具有原子性。但使用synchronized能够保证原子性。

示例:

class Demo{
    static volatile int count=0;

    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            for (int i=0;i<5000;i++){
                count++;
            }
        });
        Thread t2=new Thread(()->{
            for (int i=0;i<5000;i++){
                count++;
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("count="+count);
    }
}

 

可以看到,即使使用volatile,也不能让count的值为1w。

7.wait和notify

由于线程之间是抢占式执行、随机调度的,因此线程执行的先后顺序是无法确定的。但在实际开发中我们有时候希望能够协调多个线程之间的先后执行顺序。

wait和notify都是Object中的方法,任意的Object类对象都可以使用者两个方法

7.1wait

wait可以让调用的线程进入阻塞状态。

wait一共会做三件事:

  1. 释放锁
  2. 进入阻塞状态,等待通知
  3. 收到通知之后,唤醒线程,并重新尝试获取锁

我们来看个例子,既然wait和notify都是object中的方法那么能够直接使用吗?

class Demo14{
    static Object lock=new Object();
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            System.out.println("t1 线程开始");
            try {
                lock.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println();
        });
        t1.start();
    }
}

 我们在前面提到了wait需要做三件事,第一件事就是释放锁,说明在使用wait时,需要给他加锁才能够使用。

class Demo14{
    static Object lock=new Object();
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            System.out.println("t1 线程开始");
            synchronized (lock){
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println();
        });
        t1.start();
    }
}

 当我们运行上面的代码之后,可以看到线程t1现在是处于WAITTING(不超时的等待),但由于现在没有其他的线程去notify,因此t1会一直等下去。

7.2notify

使用notify去通知t1线程并让wait去唤醒线程。

/**
 * Demo14 类用于演示使用对象锁和等待/通知机制进行线程通信的简单示例。
 */
class Demo14{
    /**
     * 用于线程同步的锁对象。
     */
    static Object lock=new Object();
    
    /**
     * 程序入口点。
     * 创建两个线程,一个线程负责等待,另一个线程负责通知。
     * @param args 命令行参数
     */
    public static void main(String[] args) {
        /* 创建线程 t1,该线程打印开始信息后进入同步块并等待 */
        Thread t1=new Thread(()->{
            System.out.println("t1 线程开始");
            synchronized (lock){
                try {
                    /* t1 线程释放锁并等待,直到被通知 */
                    lock.wait();
                } catch (InterruptedException e) {
                    /* 将中断异常转换为运行时异常并抛出 */
                    throw new RuntimeException(e);
                }
            }
            System.out.println("t1 线程结束");
        });
        
        /* 创建线程 t2,该线程读取用户输入后进入同步块并唤醒等待中的线程 */
        Thread t2=new Thread(()->{
            Scanner scanner=new Scanner(System.in);
            scanner.nextInt();
            synchronized (lock){
                /* t2 线程唤醒等待中的线程后离开同步块 */
                lock.notify();
            }
            System.out.println("t2结束");
        });
        
        /* 启动两个线程 */
        t1.start();
        t2.start();
    }
}

这里我们设置从控制台输入,方便观察等待线程被唤醒时的现象。

 

notify和notifyAll的区别 

notify()是用于通知单个线程,notifyAll()是通知所有线程

 7.3wait和sleep的区别

wait默认是“死等”,wait也提供了带参数的版本,指定超时时间,若wait达到了最大的时间,notify还没有通知就不会继续等待下去,而是会继续执行。

wait和sleep有着本质区别:

  1. wait是为了提前唤醒线程;而sleep是固定时间的阻塞,不涉及唤醒,但sleep可以被interrupt唤醒,但调用interrupt是终止线程。
  2. wait需要搭配synchronized使用,wait会先释放锁,并同时等待;sleep和锁无关,如果不加锁sleep可以正常使用,但如果加了锁,sleep也不会释放锁,而是拉着锁一起睡眠,其他线程无法拿到锁。
  3. wait是Object的⽅法sleep是Thread的静态⽅法.

小练习

利用线程,按照顺序打印出ABC

 这里需要用到两个object对象,为什么呢?

如果我们使用同一个object,那么打印B和C的线程的顺序就是随机的,这就不符合我们的预期。可以去实验一下,使用同一个锁对象,可能就会导致打印顺序不确定。线程调度是由操作系统决定的,我们不能保证线程总是按照预期的顺序被调度。这里就不过多说。

class Demo16{
    static Object lock1=new Object();
    static Object lock2=new Object();
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            System.out.print("A");
            synchronized (lock1){
                lock1.notify();
            }
        });
        Thread t2=new Thread(()->{
            synchronized (lock1){
                try {
                    lock1.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.print("B");
            synchronized (lock2){
                lock2.notify();
            }
        });
        Thread t3=new Thread(()->{
            synchronized (lock2){
                try {
                    lock2.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("C");
        });
        t1.start();
        t2.start();
        t3.start();
    }
}

 打印10个ABC

class Demo16{
    static Object lock1=new Object();
    static Object lock2=new Object();
    static Object lock3=new Object();
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            for(int i=0;i<10;i++) {
                System.out.print("A");
                synchronized (lock1) {
                    lock1.notify();
                }
                synchronized (lock3) {
                    try {
                        lock3.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        Thread t2=new Thread(()->{
            for(int i=0;i<10;i++) {
                synchronized (lock1) {
                    try {
                        lock1.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.print("B");
                synchronized (lock2) {
                    lock2.notify();
                }
            }
        });
        Thread t3=new Thread(()->{
            for(int i=0;i<10;i++) {
                synchronized (lock2) {
                    try {
                        lock2.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("C");
                synchronized (lock3) {
                    lock3.notify();
                }
            }
        });
        t1.start();
        t2.start();
        t3.start();
    }
}

  • 32
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 9
    评论
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zhyhgx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值