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

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

一、前情概要

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

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

2Zmh5D.gif从上图可知,通过消息传递或者共享内存都可以实现线程间的通信、同步,不同的语言采用的方式可能不同,而 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()醒来后,要继续执行之前,一定要确认一下条件是否还满足,若不满足就再等,如此才是健壮的用法。

三、新的思考

  • 问题:我记得线程里还有sleep()join()yield()方法,他们也是用于线程协作嘛?
    给咱们讨论的线程协作划个边界,咱们探讨的是持锁后的线程间的协作。yield()方法通常被称为线程让步,不会释放锁,线程执行了yield()方法后,就会从运行状态转换到就绪状态。sleep()会使线程进入阻塞状态,也不会释放锁。调用目标线程实例的join()方法后,会阻塞当前线程直到目标线程中run方法运行结束。
  • 问题:Java中的同步机制不还有 JUC 中显式的 xxxLock 嘛?
    是的,Java中提供的同步机制,大致分两类,一类是内置锁synchronized,另一类就是 JUC 中的显式锁,对于显式锁的场景,则是通过Condition对象的await()方法和signal()/signaAll()方法来实现线程间的通信,同时在其之上有封装了许多更高阶的 API,更方便复杂场景的使用,其内容挺多,就算都柔在一篇中大多数读者朋友也看不完;所以吧,后续咱们再聊。

四、最后说一句

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

相关文章
|
6月前
|
消息中间件 安全 Linux
线程同步与IPC:单进程多线程环境下的选择与权衡
线程同步与IPC:单进程多线程环境下的选择与权衡
160 0
|
6月前
|
Java 开发者
多线程编程范式(一) 协作范式
多线程编程范式(一) 协作范式
|
3月前
|
Java 开发者
解锁并发编程新姿势!深度揭秘AQS独占锁&ReentrantLock重入锁奥秘,Condition条件变量让你玩转线程协作,秒变并发大神!
【8月更文挑战第4天】AQS是Java并发编程的核心框架,为锁和同步器提供基础结构。ReentrantLock基于AQS实现可重入互斥锁,比`synchronized`更灵活,支持可中断锁获取及超时控制。通过维护计数器实现锁的重入性。Condition接口允许ReentrantLock创建多个条件变量,支持细粒度线程协作,超越了传统`wait`/`notify`机制,助力开发者构建高效可靠的并发应用。
90 0
|
1月前
|
安全 Java 开发者
在多线程编程中,确保数据一致性与防止竞态条件至关重要。Java提供了多种线程同步机制
【10月更文挑战第3天】在多线程编程中,确保数据一致性与防止竞态条件至关重要。Java提供了多种线程同步机制,如`synchronized`关键字、`Lock`接口及其实现类(如`ReentrantLock`),还有原子变量(如`AtomicInteger`)。这些工具可以帮助开发者避免数据不一致、死锁和活锁等问题。通过合理选择和使用这些机制,可以有效管理并发,确保程序稳定运行。例如,`synchronized`可确保同一时间只有一个线程访问共享资源;`Lock`提供更灵活的锁定方式;原子变量则利用硬件指令实现无锁操作。
20 2
|
3月前
|
Java
多线程线程同步
多线程的锁有几种方式
|
4月前
|
安全 Java API
Java并发编程的艺术:解锁多线程同步与协作的秘密
【7月更文挑战第28天】在Java的世界中,并发编程如同一场精心编排的交响乐,每一个线程都是乐团中的乐手,而同步机制则是那指挥棒,确保旋律的和谐与统一。本文将深入探讨Java并发编程的核心概念,包括线程的创建、同步机制、以及线程间的通信方式,旨在帮助读者解锁Java多线程编程的秘密,提升程序的性能和响应性。
43 3
|
4月前
|
安全 Java 数据处理
Java并发编程:线程同步与协作的深度解析
在探索Java并发编程的海洋中,线程同步与协作的灯塔指引着航向。本文将深入挖掘线程同步机制的核心原理,揭示锁、条件变量等工具如何确保数据的一致性和线程间有序的通信。通过案例分析,我们将解码高效并发模式背后的设计哲学,并探讨现代Java并发库如何简化复杂的同步任务。跟随文章的步伐,您将获得提升多线程应用性能与可靠性的关键技能。 【7月更文挑战第24天】
45 5
|
3月前
|
安全 Java
【多线程面试题 六】、 如何实现线程同步?
实现线程同步的方法包括同步方法、同步代码块、使用ReentrantLock、volatile关键字以及原子变量类,以确保线程安全和数据一致性。
|
6月前
|
Java
Java一分钟:线程协作:wait(), notify(), notifyAll()
【5月更文挑战第11天】本文介绍了Java多线程编程中的`wait()`, `notify()`, `notifyAll()`方法,它们用于线程间通信和同步。这些方法在`synchronized`代码块中使用,控制线程执行和资源访问。文章讨论了常见问题,如死锁、未捕获异常、同步使用错误及通知错误,并提供了生产者-消费者模型的示例代码,强调理解并正确使用这些方法对实现线程协作的重要性。
64 3
|
6月前
|
安全 Linux 调度
【linux线程(二)】线程互斥与线程同步
【linux线程(二)】线程互斥与线程同步