JUC学习(三):synchronized和Lock实现线程间通信(包含虚假唤醒的讲解)

简介: JUC学习(三):synchronized和Lock实现线程间通信(包含虚假唤醒的讲解)

线程间通信的模型有两种:共享内存和消息传递,以下方式都是基本这两种模型来实现的。我们来基本一道面试常见的题目来分析 :

     

场景---两个线程,一个线程对当前数值加 1,另一个线程对当前数值减 1,要求用线程间通信


一、synchronized实现




/**
 * 实现线程A对一个值+1,线程B对该值-1
 */
//第一步:创建资源类,定义属性和操作方法
class Share{
    //目标值
    int number = 0;
    //+1操作
    public synchronized void incr() throws InterruptedException {
        //第二步:判断->操作->通知
        //判断
        if (number != 0){
            this.wait();
        }
        //操作
        number++;
        System.out.println(Thread.currentThread().getName()+":"+number);
        //通知其他线程
        this.notifyAll();
    }
    //-1操作
    public synchronized void decr() throws InterruptedException {
        if (number != 1){
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName()+":"+number);
        this.notifyAll();
    }
}
public class ThreadDemo01 {
    //第二步,创建多个线程,调用资源类中的操作方法
    public static void main(String[] args) {
        Share share = new Share();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    share.incr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "线程A").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    share.decr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "线程B").start();
    }
}



二、虚假唤醒问题


     

现在我们将进程变为四个,A、C进程负责加,B、D进程负责减  

//第一步:创建资源类,定义属性和操作方法
class Share{
    //目标值
    int number = 0;
    //+1操作
    public synchronized void incr() throws InterruptedException {
        //第二步:判断->操作->通知
        //判断
        if (number != 0){
            this.wait();
        }
        //操作
        number++;
        System.out.println(Thread.currentThread().getName()+":"+number);
        //通知其他线程
        this.notifyAll();
    }
    //-1操作
    public synchronized void decr() throws InterruptedException {
        if (number != 1){
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName()+":"+number);
        this.notifyAll();
    }
}
public class ThreadDemo01 {
    //第二步,创建多个线程,调用资源类中的操作方法
    public static void main(String[] args) {
        Share share = new Share();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    share.incr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "线程A").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    share.decr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "线程B").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    share.incr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "线程C").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    share.decr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "线程D").start();
    }
}


我们可以看到,结果并不是严格的一加一减,而是出现了2、3这样的数字,这是为什么呢?

     

这就是要讲的虚假唤醒的问题。

     

查阅jdk1.8版本的API,可以看到wait方法中有这样一段话:

559de65802b44a1fb7ed1b53446d48a4.png


意思是,我们代码中的wait应该放在循环中来避免虚假唤醒,不应该写成:

1. if (number != 0){
2. this.wait();
3. }


而应该写成:

1. while (number != 0){
2. this.wait();
3. }


为什么会导致这样呢?什么是虚假唤醒呢?

     

简而言之,就是wait()方法在唤醒之后,会直接在之前等待的地方继续执行,而不会再执行前面的判断了,这就叫做虚假唤醒。所以要放在循环中,让他唤醒后重新去做判断,避免虚假唤醒的问题。


所以Share类中正确的代码应是:

class Share{
    //目标值
    int number = 0;
    //+1操作
    public synchronized void incr() throws InterruptedException {
        //第二步:判断->操作->通知
        //判断
        while (number != 0){
            this.wait();
        }
        //操作
        number++;
        System.out.println(Thread.currentThread().getName()+":"+number);
        //通知其他线程
        this.notifyAll();
    }
    //-1操作
    public synchronized void decr() throws InterruptedException {
        while (number != 1){
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName()+":"+number);
        this.notifyAll();
    }
}


运行结果:

线程A:1
线程B:0
线程A:1
线程B:0
线程C:1
线程D:0
线程A:1
线程D:0
线程C:1
线程B:0
线程C:1
线程D:0
线程A:1
线程D:0
线程C:1
线程B:0
线程C:1
线程D:0
线程A:1
线程D:0
线程C:1
线程B:0
线程C:1
线程D:0
线程A:1
线程D:0
线程C:1
线程B:0
线程C:1
线程D:0
线程A:1
线程D:0
线程C:1
线程B:0
线程A:1
线程B:0
线程A:1
线程B:0
线程A:1
线程B:0
Process finished with exit code 0


三、Lock实现四线程操作



/**
 * Lock实现:线程A、C将number值从0变为1,线程B、D将number值从1变为0
 */
class Share{
    private int number = 0;
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    //+1操作
    public void incr() throws InterruptedException {
        lock.lock();
        try{
            while(number != 0){
                condition.await();
            }
            number++;
            System.out.println(Thread.currentThread().getName()+":"+number);
            condition.signalAll();
        }finally {
            lock.unlock();
        }
    }
    //-1操作
    public void decr() throws InterruptedException {
        lock.lock();
        try {
            while(number != 1){
                condition.await();
            }
            number--;
            System.out.println(Thread.currentThread().getName()+":"+number);
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }
}
public class ThreadDemo02 {
    public static void main(String[] args) {
        Share share = new Share();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    share.incr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "线程A").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    share.decr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "线程B").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    share.incr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "线程C").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    share.decr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "线程D").start();
    }
}


运行结果:

线程A:1
线程D:0
线程A:1
线程B:0
线程C:1
线程D:0
线程A:1
线程B:0
线程C:1
线程D:0
线程A:1
线程B:0
线程C:1
线程D:0
线程A:1
线程B:0
线程C:1
线程D:0
线程A:1
线程B:0
线程C:1
线程D:0
线程A:1
线程B:0
线程C:1
线程D:0
线程A:1
线程B:0
线程C:1
线程D:0
线程A:1
线程B:0
线程C:1
线程D:0
线程A:1
线程B:0
线程C:1
线程D:0
线程C:1
线程B:0



相关文章
|
21天前
|
Java 调度
[Java]线程生命周期与线程通信
本文详细探讨了线程生命周期与线程通信。文章首先分析了线程的五个基本状态及其转换过程,结合JDK1.8版本的特点进行了深入讲解。接着,通过多个实例介绍了线程通信的几种实现方式,包括使用`volatile`关键字、`Object`类的`wait()`和`notify()`方法、`CountDownLatch`、`ReentrantLock`结合`Condition`以及`LockSupport`等工具。全文旨在帮助读者理解线程管理的核心概念和技术细节。
34 1
[Java]线程生命周期与线程通信
|
7天前
|
Java
JAVA多线程通信:为何wait()与notify()如此重要?
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是实现线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件满足时被唤醒,从而确保数据一致性和同步。相比其他通信方式,如忙等待,这些方法更高效灵活。 示例代码展示了如何在生产者-消费者模型中使用这些方法实现线程间的协调和同步。
21 3
|
12天前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
40 4
|
22天前
|
安全 Java
Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧
【10月更文挑战第20天】Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧,包括避免在循环外调用wait()、优先使用notifyAll()、确保线程安全及处理InterruptedException等,帮助读者更好地掌握这些方法的应用。
15 1
|
22天前
|
安全 Java 开发者
Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
33 1
|
22天前
|
Java
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件成立时被唤醒,从而有效解决数据一致性和同步问题。本文通过对比其他通信机制,展示了 `wait()` 和 `notify()` 的优势,并通过生产者-消费者模型的示例代码,详细说明了其使用方法和重要性。
24 1
|
1月前
|
Java
|
1月前
|
Java 开发者
在 Java 多线程编程中,Lock 接口正逐渐取代传统的 `synchronized` 关键字,成为高手们的首选
【10月更文挑战第6天】在 Java 多线程编程中,Lock 接口正逐渐取代传统的 `synchronized` 关键字,成为高手们的首选。相比 `synchronized`,Lock 提供了更灵活强大的线程同步机制,包括可中断等待、超时等待、重入锁及读写锁等高级特性,极大提升了多线程应用的性能和可靠性。通过示例对比,可以看出 Lock 接口通过 `lock()` 和 `unlock()` 明确管理锁的获取和释放,避免死锁风险,并支持公平锁选择和条件变量,使其在高并发场景下更具优势。掌握 Lock 接口将助力开发者构建更高效、可靠的多线程应用。
24 2
|
1月前
|
Java C++
【多线程】JUC的常见类,Callable接口,ReentranLock,Semaphore,CountDownLatch
【多线程】JUC的常见类,Callable接口,ReentranLock,Semaphore,CountDownLatch
33 0
|
2月前
|
存储 缓存 安全
【Java面试题汇总】多线程、JUC、锁篇(2023版)
线程和进程的区别、CAS的ABA问题、AQS、哪些地方使用了CAS、怎么保证线程安全、线程同步方式、synchronized的用法及原理、Lock、volatile、线程的六个状态、ThreadLocal、线程通信方式、创建方式、两种创建线程池的方法、线程池设置合适的线程数、线程安全的集合?ConcurrentHashMap、JUC
【Java面试题汇总】多线程、JUC、锁篇(2023版)