多线程编程一直是普通程序员进阶为高级程序员的必备技能之一!他很难,难就难在难以理解、难以调试,出现了bug很难发现及排查。他很重要,因为我们可能随时都面对着线程的切换、调度,只是这些都由CPU来帮我们完成我们无法感知。
记得我在刚开始学C语言的时候,只会在黑窗口里面打印一个helloworld、打印一个斐波拉契数列、打印乘法口诀表。当时觉得很枯燥,也不知道这个能学来干嘛。等到后面工作中才发现这些都是基础,有了这些基础才能做更高级一点的开发!其实多线程编程也是一样,学习基础的时候很枯燥,也不知道学了能干嘛。我不会多线程编程不也一样能写CRUD么,不也能实现工作中的需求么?但是要是想去大厂或者互联网公司,不会多线程可能就直接被pass掉了吧!
本文不会讲什么是线程、什么是进程这些概念,不清楚这些概念的可以自己先去学习下,文中的代码都由java语言编写!
线程的基本状态
开局一张图,内容全靠“编”!我们先来看一张图。
这张图参考网上看到的一张图,它比较详细的描述了线程的生命周期以及几种基本状态。这张图我分了两块区域来讲,我们接下来先分析下区域一。
目前正值新冠肺炎肆虐之际,口罩一时成了出门必备的稀缺之物!假设我们有个生产口罩的工厂,能够生产及销售口罩。
class MaskFactory {
public void produce() throws InterruptedException {
// #3
// Thread.sleep(1000);
System.out.println("生产了1个口罩");
}
public void consume() {
System.out.println("消费了1个口罩");
}
}
public class Demo1 {
public static void main(String[] args) {
final MaskFactory maskFactory = new MaskFactory();
// #1
Thread threadA = new Thread(new Runnable() {
public void run() {
try {
maskFactory.produce();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// #2
threadA.start();
}
}
上述代码中,我们创建了一个线程去生产口罩,那线程的的状态会经过什么样的变化呢?
区域一的5个状态我们前面都已经分析过了,算是比较好理解的!区域二的2个状态就稍微有点复杂了,分别是Blocked in Object's Lock Pool和Blocked in Object's Wait Pool。这两个状态的共同点是Blocked in Object's ...,这里的object是什么呢?什么对象?谁的对象?其实这就跟我们的锁密切相关了。从名字我们能看出来,其实这两个状态也属于阻塞状态,但是与我们上面说过的通过sleep()阻塞又不一样!
线程间的通信
我们继续改进上面的代码,并且启动两个线程,一个负责生产口罩一个负责消费口罩,分别执行10次。并且生产线程和消费线程必须交替的执行!代码如下:
class MaskFactory {
private int number = 0;
// 生产
public synchronized void produce() {
if (number != 0) {
try {
// 等待
this.wait();
} catch (InterruptedException e) {
}
}
number++;
System.out.println("生产线程" + Thread.currentThread().getName() + "执行,目前口罩数量:" + number);
// 通知
this.notifyAll();
}
// 消费
public synchronized void consume() {
if (number == 0) {
try {
// 等待
this.wait();
} catch (InterruptedException e) {
}
}
number--;
System.out.println("消费线程" + Thread.currentThread().getName() + "执行,目前口罩数量:" + number);
// 通知
this.notifyAll();
}
}
public class Demo1 {
public static void main(String[] args) {
final MaskFactory maskFactory = new MaskFactory();
// 1
new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 10; i++) {
maskFactory.produce();
}
}
}, "A").start();
// 2
new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 10; i++) {
maskFactory.consume();
}
}
}, "B").start();
}
}
执行结果如何呢?
接下来我们就结合代码和之前,我们需要注意下面几点:
用过synchronized关键字的宝宝应该知道,在非静态方法上,锁定的是实例对象。wait和notifyAll方法前面的this也指向当前实例。我们进入wait方法和notifyAll方法发现他们都是object基类的方法。也就是说在java中任何对象都有这两个方法。既然这样,那我们可以可以随便new一个对象,然后在其中调用wait和notifyAll方法呢?答案是不行的!因为我们的等待和通知都是跟锁相关的,所以synchronized锁定的对象以及调用wait和notifyAll方法的对象必须是同一个!
基于上面的前提我们再来分析一下另外两个阻塞状态:
线程间的虚假唤醒
到这里一切好像都很完美,如果线程A线程不满足条件则进行等待B线程执行后唤醒。线程B线程不满足条件则进行等待A线程执行后唤醒。那我们再多加两条线程C、D执行会出现什么样的结果呢?代码如下(MaskFactory类的代码与上一个例子一样):
class MaskFactory {
private int number = 0;
private int index = 1;
// 生产
public synchronized void produce() {
if (number != 0) {
try {
// 等待
this.wait();
} catch (InterruptedException e) {
}
}
number++;
System.out.println("第"+ index++ + "行:生产线程" + Thread.currentThread().getName() + "执行,目前口罩数量:" + number);
// 通知
this.notifyAll();
}
// 消费
public synchronized void consume() {
if (number == 0) {
try {
// 等待
this.wait();
} catch (InterruptedException e) {
}
}
number--;
System.out.println("第"+ index++ + "行:消费线程" + Thread.currentThread().getName() + "执行,目前口罩数量:" + number);
// 通知
this.notifyAll();
}
}
public class Demo1 {
public static void main(String[] args) {
final MaskFactory maskFactory = new MaskFactory();
// 1
new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 10; i++) {
maskFactory.produce();
}
}
}, "A").start();
// 2
new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 10; i++) {
maskFactory.consume();
}
}
}, "B").start();
// 3
new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 10; i++) {
maskFactory.produce();
}
}
}, "C").start();
// D
new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 10; i++) {
maskFactory.consume();
}
}
}, "D").start();
}
}
线程A、C负责生产,D、B线程负责消费,那输出结果是什么样子的呢?思考一分钟然后看结果:
其实输出结果我们谁也没法预期会怎么输出!但是可以确定的一点是生产线程和消费线程不一定会交替执行,也就是说不一定会按照01010101规律输出! 为什么说不一定呢?难道说也有可能会交替输出么?是的,我们多运行几次代码发现每次输出的结果都可能不一样!我们就结合上面的结果来分析一下:
整个过程有点绕,不清楚的得多看几遍才能慢慢理解。一个大前提是前面说到过的已经准备就绪的线程,随时可能执行!至于什么时候能执行呢?这个是不确定的,完全取决于CPU的调度!上述的现象产生的原因就是线程的虚假唤醒,也就是本不应该唤醒的线程被唤醒了,因此输出出现异常!那这样的问题该怎么解决呢?其实很简单:
//将if替换为while
while (number != 0) {
try {
// 等待
this.wait();
} catch (InterruptedException e) {
}
}
只需要将之前代码中的if替换为while,当每次唤醒之后不是继续往下执行,而是再判断一次状态是否符合条件,不符合条件则继续等待!
精准通知顺序访问
我们使用notifyAll的时候,只要是在等待池中的线程都会一股脑的全部都通知。那么这里我们思考一下,当等待池中有消费线程也有生产线程的时候,我们是不是可以在生产线程执行后只通知消费线程,在消费线程执行后只通知生产线程呢?
如果要实现精准的通知,那就得用另外一套锁逻辑了,我们先看实现代码(创建线程的逻辑与前面类似,为节约篇幅这里不再列出):
class MaskFactory {
private int number = 0;
private Lock lock = new ReentrantLock();
Condition consumeCondition = lock.newCondition();
Condition produceContion = lock.newCondition();
// 生产
public void produce() {
lock.lock();
try {
while (number != 0) {
// 等待一个生产的信号
produceContion.await();
}
number++;
System.out.println("生产线程" + Thread.currentThread().getName() + "执行,目前口罩数量:" + number);
// 发出消费的信号
consumeCondition.signalAll();
} catch (Exception e) {
} finally {
lock.unlock();
}
}
// 消费
public void consume() {
lock.lock();
try {
while (number == 0) {
// 等待一个消费的信号
consumeCondition.await();
}
number--;
System.out.println("消费线程" + Thread.currentThread().getName() + "执行,目前口罩数量:" + number);
// 发出生产的信号
produceContion.signalAll();
} catch (Exception e) {
} finally {
lock.unlock();
}
}
}
这里我们就使用了ReentrantLock来替代synchronized,一个ReentrantLock对象可以创建多个通知条件,也就是代码中的consumeCondition和produceContion。当生产线程调用produce时发现不满足条件,则会执行produceContion.await()进行等待,直到有消费线程调用produceContion.signalAll()时,生产线程才会被唤醒!这也就实现了线程的精准通知!因此用ReentrantLock来替代synchronized可以更加灵活的控制线程!
常见面试题
下面我们来看几个可能会遇到的面试题