【自省】线程同步看腻了,尝尝 > 入门级的线程间协作?

3,037 阅读8分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第 21 天,点击查看活动详情

我是石页兄,本篇不带情感,只聊干货

欢迎关注微信公众号「架构染色」交流和学习


》》》归属专栏:《高并发的暴力美学》

一、前情概要

《并发之道:三大并发问题与 JMM 何干?》中,在讨论【编程语言如何解决并发问题】的话题时,有聊到编程语言面对并发的三大问题,它所做的是对多线程的通信、同步机制进行包装,为开发者提供使用轻便、功能丰富的多线程编程 API。而 API 只是上层工具,本质是要选择合适的多线程通信、同步机制:

  • 线程间通信:线程间交换信息的机制
  • 线程间同步:控制不同线程之间操作发生相对顺序的机制

图片

从上图可知,通过消息传递或者共享内存都可以实现线程间的通信、同步,不同的语言采用的方式可能不同,而 JAVA 采用的是共享内存的方式。

有个朋友看到这个之后有来探讨这个知识点,表示同步理解起来比较容易,比如加锁保持逻辑串行。可是这里所描述的【通信】则有点模糊,不太能 get,于是两人之间讨论一番,之后将讨论内容整理形成本篇。

二、你来我往的讨论

  • 问题:线程之间通信是干嘛,日常工作中用的着他嘛?

    操作系统中线程间协作的核心是通信,而这个通信就是上边所描述的,线程之间交换、传递信息的机制。

  • 问题:怎么又冒出来个线程间协作,是什么的?

    在操作系统的概念定义中,线程协作是指多个线程之间相互协作,共同完成任务。比如生产者消费者场景下,一个线程负责生产数据,另一个线程负责消费数据。消费者线程在消费数据前,必须等待生产者线程把数据准备好。

  • 问题:这听起来跟线程间互同步的例子有点类似呢?这两者是一回事呢?

    Java 中,线程间的通信与同步是你中有我的关系,即线程间的通信,要在同步逻辑内(持锁后、释放锁之前)实现。

  • 问题:是不是说通信方法的调用要要在synchronized内?

    是的,Java中最早期提供的同步机制就是内置锁synchronized,在synchronized中通过Object对象提供的wait()方法以及notify()/notifyAll()方法实现线程间的通信。另外随着JDK不断地升级,这个内置锁的性能也越来越好。

  • 问题:Objectwait干嘛用?

    如果当前线程在等待满足某个条件后,才能继续执行,但是又必须依赖其它线程来触发条件;这种情况下需要考虑调用wait()方法,将当前线程挂起,等待条件满足后再继续执行。使用时的关键逻辑是:

    1. 先进入同步代码块,即拿到锁
    2. 调用 xxx.wait(); 把当前线程放入锁对象xxx的wait set中挂起,之后释放锁(sleep()方法也能挂起线程,但挂起后不会释放锁)
    3. 等待别的线程调用 通知方法给信号后,刚执行wait的线程 从锁对象xxx的 wait set 中移除,放入锁池队列中,被系统调度唤醒后重新持有锁,继续执行wait之后的代码
    4. 通知方法 包括notifynotifyAll,注意interrupt也基于通知的能力
  • 问题:这里wait set和锁池队列都是什么呢?

    这里暂且先简单理解为两个不同状态标识的集合,JVM 将线程对象在这些集合之间迁移以辅助完成线程的调度执行以及状态的变更,具体的细节后续结合内部实现原理以及线程状态设计等内容再聊。

  • 问题:Objectnotify /notifyall怎么用?

    notify /notifyall用于通知执行wait后挂起等待的线程醒来,继续执行后续逻辑。其使用时的关键逻辑是:

    1. 先进入同步代码块,即拿到锁
    2. notify /notifyall给等待中的线程发信号,区别在于:notify只从 wait set中移动一个线程到锁池中,notifyAll是将 wait set中的全部线程都移到锁池中。
    3. 之后继续执行notify/notifyAll 后的方法,直到退出同步代码块后才释放锁。这里很关键,容易踩坑的点是:发送通知后,线程不会终止执行,而是继续执行,所以后续的代码可能还会修改竞态条件
  • 问题:什么是竞态条件?

    竞态条件是指在并发环境中,当有多个线程同时访问同一个临界资源时,由于多个线程的并发执行顺序的不确定,从而导致程序输出结果的不确定,这种情况我们称之为竞态条件 (Race Conditions)

    比如 if (a > 0),a > 0 就是竞态条件,只有当 a > 0 时才执行什么逻辑,那么 a 的值如果被修改成 < 0 ,就可以理解为上述的修改竞态条件。

  • 问题:wait的线程醒来后,就继续执行,它都不再确认一下条件是否真的满足嘛?

    朋友的这个问题,真的是关键中的关键,从注释中寻找线索

    As in the one argument version, interrupts and spurious wakeups are possible, and this method should always be used in a loop:
      synchronized (obj) {
          while (<condition does not hold>)
              obj.wait();
          ... // Perform action appropriate to condition
      }
    

    注释spurious wakeups是虚假唤醒的意思,什么是虚假唤醒呢?

    虚假唤醒是一种现象,它只会出现在多线程环境中,指的是在多线程环境下,多个线程等待在同一个条件上,等到条件满足时,所有等待的线程都被唤醒,但由于多个线程执行的顺序不同,后面竞争到锁的线程在获得时间片时条件已经不再满足,线程应该继续睡眠但是却继续往下运行的一种现象。

  • 问题:听起来挺干瘪的,能模拟个示例嘛?

    1. 案例 1
    public class WaitNotifyDemo {
    
        private volatile  boolean isEmpty= false;
    
        /**
         * 假唤醒实例1 ,提供消费条件,唤醒消费线程,又把消费条件给取消了,这样消费线程醒来,缺不满足条件.
         */
        public synchronized void fakeProvider1(){
            System.out.println("冒牌provider 把isEmpty 设置为true,通知后,还把信号再修改为false");
            isEmpty = true;
            notify();//发送通知后,线程不会退出
            isEmpty = false; //说不清的原因 信号又被重置了.这种情况下,consumer线程醒来后,其实isEmpty条件是不满足的,所以应该应用用while循环来判断条件
        }
        public synchronized void consumer1() throws InterruptedException {
            //使用while循环来防止假唤醒.所谓的假唤醒本质是,唤醒后不满足继续执行的条件,所以继续判断下条件是否满足,不满足就继续wait.
            while (isEmpty){
                //条件不满足,等待
                this.wait();
            }
            System.out.println("成功消费一次,isEmpty 设置为true");
            isEmpty = true;
        }
    }
    
    1. 案例 2
    public class WaitNotifyDemo2 {
    
        private volatile  boolean isEmpty= false;
        /**
         * 一个provider
         * 多个线程执行消费
         * 正常的设置消费条件.发通知
         */
        public synchronized void provider(){
            System.out.println("provider 把isEmpty 设置为true,通知所有等待线程");
            isEmpty = true;
            notifyAll();
        }
    
        /**
         * 多个消费者都唤醒后,其中1个消费者拿到锁,执行消费后,把状态重置了.另外一个消费者的执行条件就不满足了,要继续等.
         * @throws InterruptedException
         */
        public synchronized void consumer() throws InterruptedException {
            //使用while循环来防止假唤醒.所谓的假唤醒本质是,唤醒后不满足继续执行的条件,所以继续判断下条件是否满足,不满足就继续wait.
            while (isEmpty){
                //条件不满足,等待
                this.wait();
            }
            System.out.println("成功消费一次,isEmpty 设置为true");
            isEmpty = true;
        }
    }
    

    所以,这里强调的重点正是问题的关注点,当线程从wait()醒来后,要继续执行之前,一定要确认一下条件是否还满足,若不满足就再等,如此才是健壮的用法。

image.png

三、新的思考

  • 问题:我记得线程里还有sleep()join()yield()方法,他们也是用于线程协作嘛?

    给咱们讨论的线程协作划个边界,咱们探讨的是持锁后的线程间的协作。yield()方法通常被称为线程让步,不会释放锁,线程执行了yield()方法后,就会从运行状态转换到就绪状态。sleep()会使线程进入阻塞状态,也不会释放锁。调用目标线程实例的join()方法后,会阻塞当前线程直到目标线程中run方法运行结束。

  • 问题:Java中的同步机制不还有 JUC 中显式的 xxxLock 嘛?

    是的,Java中提供的同步机制,大致分两类,一类是内置锁synchronized,另一类就是 JUC 中的显式锁,对于显式锁的场景,则是通过Condition对象的await()方法和signal()/signaAll()方法来实现线程间的通信,同时在其之上有封装了许多更高阶的 API,更方便复杂场景的使用,其内容挺多,就算都柔在一篇中大多数读者朋友也看不完;所以吧,后续咱们再聊。

    image.png

四、最后说一句

我是石页兄,如果这篇文章对您有帮助,或者有所启发的话,欢迎关注笔者的微信公众号【 架构染色 】进行交流和学习。您的支持是我坚持写作最大的动力。

欢迎点击链接扫马儿关注、交流。