一、进程线程与管程

1.1 进程

根据理解,我们可以对进程下个定义——进程是运行中的程序。注意,进程是一个动态的概念,这一点很重要。但是进程并不是那一段可执行的程序代码,在操作系统上进程的本质是一组有序指令加数据以及资源的集合。操作系统上,进程是以进程主体和相应的进程控制块(PCB)形式存在的,其中进程控制块包括程序计数器,程序上下文,程序资源(文件、信号等)组成

1.2 线程

所谓的线程实际上是进程内部的一条执行序列,也叫做执行流。执行序列是指一组有序指令加数据的集合,执行序列是以函数为单位的。线程是一种轻量级的进程。线程一定是在进程内部进行活动的,并且每一个线程都拥有一个独立的计数器、进程栈和一组进程寄存器。强调一点,进程调度的对象是线程,而不是进程

1.3 进程与线程的区别

  1. 进程是系统资源分配的最小单位,线程是CPU调度的最小单位
    2. 一个进程可以包含多条线程,而一个线程只能属于一个进程
    3. 创建进程消耗的资源要比创建线程消耗的资源大得多
    4. 进程的切换效率要比线程的切换效率低得多
    5. 系统中每一个进程都是相互独立的存在,而同一个进程中所有的线程只有自己的栈区,其他空间都是共享的
    6. 进程之间的通讯必须借助于外部手段,而线程之间的通讯是直接通过共享空间来通讯的
    7. 进程之间不存在安全问题,而同一个进程中的线程存在安全问题

1.4 管程

Monitor其实是一种同步机制,他的义务是保证(同一时间)只有一个线程可以访问被保护的数据和代码
JVM中同步是基于进入和退出监视器对象(Monitor,管程对象)来实现的,每个对象实例都会有一个Monitor对象

  1. Object o = new Object();
  2. new Thread(() -> {
  3. synchronized (o) {
  4. }
  5. },"t1").start();

Monitor对象会和Java对象一同创建并销毁,它底层是由C++语言来实现的。

1.5 协程

相比于前面的进程和线程,协程是一种用户态的轻量级线程。协程的调度由用户控制,拥有自己独立的寄存器上下文和栈。协程的切换效率比线程还要高!
介绍完协程,我们来说一下协程和线程的区别

  • 线程程是由CPU调度,而协程是由用户调度
  • 线程存在安全问题,协程比线程较安全
  • 线程使用同步机制,协程使用异步机制

二、线程的状态

Thread有五种状态类型

状态名称 说明
NEW 初始状态,线程被构建,但未调用start()方法
RUNNABLE 运行状态,调用start()方法后。在java线程中,将操作系统线程的就绪和运行统称运行状态
BLOCKED 阻塞状态,线程等待进入synchronized代码块或方法中,等待获取锁
WAITING 等待状态,线程可调用wait、join等操作使自己陷入等待状态,并等待其他线程做出特定操作(如notify或中断)
TIMED_WAITING 超时等待,线程调用sleep(timeout)、wait(timeout)等操作进入超时等待状态,超时后自行返回
TERMINATED 终止状态,线程运行结束

image.png
当我们调用线程类的 sleep()、suspend()、yield()、wait() 等方法时会导致线程进入阻塞状态

三、线程等待与唤醒

3.1 Wait/Notify通知机制解析

3.1.1 概念

  • wait():调用任何对象的wait()方法会让当前线程进入等待,直到另一个线程调用同一个对象的notify()或notifyAll()方法
  • notify():唤醒因调用这个对象wait()方法而阻塞的线程

首先,sleep()、suspend()、yield () 等方法都隶属于 Thread 类,但 wait()/notify() 这一对却直接隶属于Object 类,也就是说,所有对象都拥有这一对方法。初看起来这十分不可思议,但是实际上却是很自然的,因为这一对方法阻塞时要释放占用的锁,而锁是任何对象都具有的,调用对象的 wait() 方法导致线程阻塞,并且该对象上的锁被释放。而调用对象的notify()方法则导致因调用该对象的 wait() 方法而阻塞的线程中随机选择的一个解除阻塞(但要等到获得锁后才真正可执行)

其次,前面叙述的所有方法都可在任何位置调用,但是这一对方法却必须在 synchronized 方法或块中调用,理由也很简单,只有在 synchronized 方法或块中当前线程才占有锁,才有锁可以释放。同样的道理,调用这一对方法的对象上的锁必须为当前线程所拥有,这样才有锁可以释放。因此,这一对方法调用必须放置在这样的 synchronized 方法或块中,该方法或块的上锁对象就是调用这一对方法的对象。若不满足这一条件,则程序虽然仍能编译,但在运行时会出现 IllegalMonitorStateException 异常

注意:

  • 调用 notify() 方法导致解除阻塞的线程是从因调用该对象的 wait() 方法而阻塞的线程中随机选取的,我们无法预料哪一个线程将会被选择,所以编程时要特别小心,避免因这种不确定性而产生问题
  • 除了 notify(),还有一个方法 notifyAll() 也可起到类似作用,唯一的区别在于,调用 notifyAll() 方法将把因调用该对象的 wait() 方法而阻塞的所有线程一次性全部解除阻塞。当然,只有获得锁的那一个线程才能进入可执行状态。
  • wait()和notify()必须成对存在。

3.1.2 wait/notify用例

让我们先通过一个示例解析
wait()方法可以使线程进入等待状态,而notify()可以使等待的状态唤醒。这样的同步机制十分适合生产者、消费者模式:消费者消费某个资源,而生产者生产该资源。当该资源缺失时,消费者调用wait()方法进行自我阻塞,等待生产者的生产;生产者生产完毕后调用notify/notifyAll()唤醒消费者进行消费。

以下是代码示例,其中flag标志表示资源的有无

  1. public class ThreadTest {
  2. static final Object obj = new Object(); //对象锁
  3. private static boolean flag = false;
  4. public static void main(String[] args) throws Exception {
  5. Thread consume = new Thread(new Consume(), "Consume");
  6. Thread produce = new Thread(new Produce(), "Produce");
  7. consume.start();
  8. Thread.sleep(1000);
  9. produce.start();
  10. try {
  11. produce.join();
  12. consume.join();
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }
  16. }
  17. // 生产者线程
  18. static class Produce implements Runnable {
  19. @Override
  20. public void run() {
  21. synchronized (obj) {
  22. System.out.println("进入生产者线程");
  23. System.out.println("生产");
  24. try {
  25. TimeUnit.MILLISECONDS.sleep(2000); //模拟生产过程
  26. flag = true;
  27. obj.notify(); //通知消费者
  28. TimeUnit.MILLISECONDS.sleep(1000); //模拟其他耗时操作
  29. System.out.println("退出生产者线程");
  30. } catch (InterruptedException e) {
  31. e.printStackTrace();
  32. }
  33. }
  34. }
  35. }
  36. //消费者线程
  37. static class Consume implements Runnable {
  38. @Override
  39. public void run() {
  40. synchronized (obj) {
  41. System.out.println("进入消费者线程");
  42. System.out.println("wait flag 1:" + flag);
  43. while (!flag) { //判断条件是否满足,若不满足则等待
  44. try {
  45. System.out.println("还没生产,进入等待");
  46. obj.wait();
  47. System.out.println("结束等待");
  48. } catch (InterruptedException e) {
  49. e.printStackTrace();
  50. }
  51. }
  52. System.out.println("wait flag 2:" + flag);
  53. System.out.println("消费");
  54. System.out.println("退出消费者线程");
  55. }
  56. }
  57. }
  58. }

输出结果为:

进入消费者线程 

wait flag 1:false 

还没生产,进入等待 

进入生产者线程 

生产 

退出生产者线程 

结束等待 

wait flag 2:true 

消费 

退出消费者线程

理解了输出结果的顺序,也就明白了wait/notify的基本用法。有以下几点需要知道:

  1. 在示例中没有体现但很重要的是,wait/notify方法的调用必须处在该对象的锁(Monitor)中,也即,在调用这些方法时首先需要获得该对象的锁。否则会抛出IllegalMonitorStateException异常。
  2. 从输出结果来看,在生产者调用notify()后,消费者并没有立即被唤醒,而是等到生产者退出同步块后才唤醒执行。(这点其实也好理解,synchronized同步方法(块)同一时刻只允许一个线程在里面,生产者不退出,消费者也进不去)
  3. 注意,消费者被唤醒后是从wait()方法(被阻塞的地方)后面执行,而不是重新从同步块开始。

3.2.3 深入了解

这一节我们探讨wait/notify与线程状态之间的关系。深入了解线程的生命周期。
由前面线程的状态转化图可知,当调用wait()方法后,线程会进入WAITING(等待状态),后续被notify()后,并没有立即被执行,而是进入等待获取锁的阻塞队列。
image.png
对于每个对象来说,都有自己的等待队列和阻塞队列。以前面的生产者、消费者为例,我们拿obj对象作为对象锁,配合图示。内部流程如下

  1. 当线程A(消费者)调用wait()方法后,线程A让出锁,自己进入等待状态,同时加入锁对象的等待队列。
  2. 线程B(生产者)获取锁后,调用notify方法通知锁对象的等待队列,使得线程A从等待队列进入阻塞队列。
  3. 线程A进入阻塞队列后,直至线程B释放锁后,线程A竞争得到锁继续从wait()方法后执行。

3.2 Condition的await和signal等待/通知机制

3.2.1 Condition简介

任何一个java对象都天然继承于Object类,在线程间实现通信的往往会应用到Object的几个方法,比如wait(),wait(long timeout),wait(long timeout, int nanos)与notify(),notifyAll()几个方法实现等待/通知机制,同样的, 在java Lock体系下依然会有同样的方法实现等待/通知机制。从整体上来看Object的wait和notify/notify是与对象监视器配合完成线程间的等待/通知机制,而Condition与Lock配合完成等待通知机制,前者是java底层级别的,后者是语言级别的,具有更高的可控制性和扩展性。两者除了在使用方式上不同外,在功能特性上还是有很多的不同:

  • Condition能够支持不响应中断,而通过使用Object方式不支持;
  • Condition能够支持多个等待队列(new 多个Condition对象),而Object方式只能支持一个;
  • Condition能够支持超时时间的设置,而Object不支持

参照Object的wait和notify/notifyAll方法,Condition也提供了同样的方法:
针对Object的wait方法

  • void await() throws InterruptedException:当前线程进入等待状态,如果其他线程调用condition的signal或者signalAll方法并且当前线程获取Lock从await方法返回,如果在等待状态中被中断会抛出被中断异常;
  • long awaitNanos(long nanosTimeout):当前线程进入等待状态直到被通知,中断或者超时
  • boolean await(long time, TimeUnit unit) throws InterruptedException:同第二种,支持自定义时间单位
  • boolean awaitUntil(Date deadline) throws InterruptedException:当前线程进入等待状态直到被通知,中断或者到了某个时间

针对Object的notify/notifyAll方法

  1. void signal():唤醒一个等待在condition上的线程,将该线程从等待队列中转移到同步队列中,如果在同步队列中能够竞争到Lock则可以从等待方法中返回。
  2. void signalAll():与1的区别在于能够唤醒所有等待在condition上的线程

3.2.2 Condition实现原理分析

参考:https://www.jianshu.com/p/28387056eeb4

3.2.2.1 等待队列

要想能够深入的掌握condition还是应该知道它的实现原理,现在我们一起来看看condiiton的源码。创建一个condition对象是通过 lock.newCondition() 而这个方法实际上是会new出一个ConditionObject对象,该类是AQS(AQS的实现原理的文章)的一个内部类,有兴趣可以去看看。前面我们说过,condition是要和lock配合使用的也就是condition和Lock是绑定在一起的,而lock的实现原理又依赖于AQS,自然而然ConditionObject作为AQS的一个内部类无可厚非。我们知道在锁机制的实现上,AQS内部维护了一个同步队列,如果是独占式锁的话,所有获取锁失败的线程的尾插入到同步队列,同样的,condition内部也是使用同样的方式,内部维护了一个 等待队列,所有调用condition.await方法的线程会加入到等待队列中,并且线程状态转换为等待状态。另外注意到ConditionObject中有两个成员变量:

/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;

这样我们就可以看出来ConditionObject通过持有等待队列的头尾指针来管理等待队列。主要注意的是Node类复用了在AQS中的Node类,其节点状态和相关属性可以去看AQS的实现原理的文章,如果您仔细看完这篇文章对condition的理解易如反掌,对lock体系的实现也会有一个质的提升。Node类有这样一个属性:

//后继节点
Node nextWaiter;

进一步说明,等待队列是一个单向队列,而在之前说AQS时知道同步队列是一个双向队列。接下来我们用一个demo,通过debug进去看是不是符合我们的猜想

public static void main(String[] args) {
    for (int i = 0; i < 10; i++) {
        Thread thread = new Thread(() -> {
            lock.lock();
            try {
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        });
        thread.start();
    }
}

3.3. condition.await和object.wait区别