一、进程线程与管程
1.1 进程
根据理解,我们可以对进程下个定义——进程是运行中的程序。注意,进程是一个动态的概念,这一点很重要。但是进程并不是那一段可执行的程序代码,在操作系统上进程的本质是一组有序指令加数据以及资源的集合。操作系统上,进程是以进程主体和相应的进程控制块(PCB)形式存在的,其中进程控制块包括程序计数器,程序上下文,程序资源(文件、信号等)组成
1.2 线程
所谓的线程实际上是进程内部的一条执行序列,也叫做执行流。执行序列是指一组有序指令加数据的集合,执行序列是以函数为单位的。线程是一种轻量级的进程。线程一定是在进程内部进行活动的,并且每一个线程都拥有一个独立的计数器、进程栈和一组进程寄存器。强调一点,进程调度的对象是线程,而不是进程
1.3 进程与线程的区别
- 进程是系统资源分配的最小单位,线程是CPU调度的最小单位
2. 一个进程可以包含多条线程,而一个线程只能属于一个进程
3. 创建进程消耗的资源要比创建线程消耗的资源大得多
4. 进程的切换效率要比线程的切换效率低得多
5. 系统中每一个进程都是相互独立的存在,而同一个进程中所有的线程只有自己的栈区,其他空间都是共享的
6. 进程之间的通讯必须借助于外部手段,而线程之间的通讯是直接通过共享空间来通讯的
7. 进程之间不存在安全问题,而同一个进程中的线程存在安全问题
1.4 管程
Monitor其实是一种同步机制,他的义务是保证(同一时间)只有一个线程可以访问被保护的数据和代码
JVM中同步是基于进入和退出监视器对象(Monitor,管程对象)来实现的,每个对象实例都会有一个Monitor对象
Object o = new Object();
new Thread(() -> {
synchronized (o) {
}
},"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 | 终止状态,线程运行结束 |
当我们调用线程类的 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标志表示资源的有无
public class ThreadTest {
static final Object obj = new Object(); //对象锁
private static boolean flag = false;
public static void main(String[] args) throws Exception {
Thread consume = new Thread(new Consume(), "Consume");
Thread produce = new Thread(new Produce(), "Produce");
consume.start();
Thread.sleep(1000);
produce.start();
try {
produce.join();
consume.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 生产者线程
static class Produce implements Runnable {
@Override
public void run() {
synchronized (obj) {
System.out.println("进入生产者线程");
System.out.println("生产");
try {
TimeUnit.MILLISECONDS.sleep(2000); //模拟生产过程
flag = true;
obj.notify(); //通知消费者
TimeUnit.MILLISECONDS.sleep(1000); //模拟其他耗时操作
System.out.println("退出生产者线程");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//消费者线程
static class Consume implements Runnable {
@Override
public void run() {
synchronized (obj) {
System.out.println("进入消费者线程");
System.out.println("wait flag 1:" + flag);
while (!flag) { //判断条件是否满足,若不满足则等待
try {
System.out.println("还没生产,进入等待");
obj.wait();
System.out.println("结束等待");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("wait flag 2:" + flag);
System.out.println("消费");
System.out.println("退出消费者线程");
}
}
}
}
输出结果为:
进入消费者线程
wait flag 1:false
还没生产,进入等待
进入生产者线程
生产
退出生产者线程
结束等待
wait flag 2:true
消费
退出消费者线程
理解了输出结果的顺序,也就明白了wait/notify的基本用法。有以下几点需要知道:
- 在示例中没有体现但很重要的是,wait/notify方法的调用必须处在该对象的锁(Monitor)中,也即,在调用这些方法时首先需要获得该对象的锁。否则会抛出IllegalMonitorStateException异常。
- 从输出结果来看,在生产者调用notify()后,消费者并没有立即被唤醒,而是等到生产者退出同步块后才唤醒执行。(这点其实也好理解,synchronized同步方法(块)同一时刻只允许一个线程在里面,生产者不退出,消费者也进不去)
- 注意,消费者被唤醒后是从wait()方法(被阻塞的地方)后面执行,而不是重新从同步块开始。
3.2.3 深入了解
这一节我们探讨wait/notify与线程状态之间的关系。深入了解线程的生命周期。
由前面线程的状态转化图可知,当调用wait()方法后,线程会进入WAITING(等待状态),后续被notify()后,并没有立即被执行,而是进入等待获取锁的阻塞队列。
对于每个对象来说,都有自己的等待队列和阻塞队列。以前面的生产者、消费者为例,我们拿obj对象作为对象锁,配合图示。内部流程如下
- 当线程A(消费者)调用wait()方法后,线程A让出锁,自己进入等待状态,同时加入锁对象的等待队列。
- 线程B(生产者)获取锁后,调用notify方法通知锁对象的等待队列,使得线程A从等待队列进入阻塞队列。
- 线程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方法
- void signal():唤醒一个等待在condition上的线程,将该线程从等待队列中转移到同步队列中,如果在同步队列中能够竞争到Lock则可以从等待方法中返回。
- 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();
}
}