多线程并发是Java语言中非常重要的一块内容,同时,也是Java基础的一个难点。说它重要是因为多线程是日常开发中频繁用到的知识,说它难是因为多线程并发涉及到的知识点非常之多,想要完全掌握Java的并发相关知识并非易事。也正因此,Java并发成了Java面试中最高频的知识点之一。本系列文章将从Java内存模型、volatile关键字、synchronized关键字、ReetrantLock、Atomic并发类以及线程池等方面来系统的认识Java的并发知识。通过本系列文章的学习你将深入理解volatile关键字的作用,了解到synchronized实现原理、AQS和CLH队列锁,清晰的认识自旋锁、偏向锁、乐观锁、悲观锁…等等一系列让人眼花缭乱的并发知识。
多线程并发系列文章:
这一次,彻底搞懂Java中的ReentranLock实现原理
Java并发系列番外篇:ThreadLocal原理其实很简单
本文是Java并发系列的第五篇文章,将深入分析Java的唤醒与等待机制。
关于线程的等待与唤醒想必大家都不陌生,毕竟在初学Java基础时都是重点学习的内容。在前两篇分析synchronized与ReentranLock的文章中我们略过了线程的等待与唤醒相关内容,主要是因为想要深入的理解线程的等待与唤醒机制并不容易,因此将这一知识点单独写篇文章来分析。那么本篇文章我们将从synchronized与ReentranLock两个方面来深入分下线程的等待与唤醒。
开始之前先给大家推荐一下AndroidNote这个GitHub仓库,这里是我的学习笔记,同时也是我文章初稿的出处。这个仓库中汇总了大量的java进阶和Android进阶知识。是一个比较系统且全面的Android知识库。对于准备面试的同学也是一份不可多得的面试宝典,欢迎大家到GitHub的仓库主页关注。
一、从synchronized锁看线程等待与唤醒
初学Java的时候想必大家都用synchronized实现过“生产者-消费者”模型的代码,其中用到了几个Object中的方法如wait()、notify()、notifyAll(),不知道当时的你是否有些困惑,线程等待与唤醒相关的方法为什么会定义在Object类中呢?
什么?你连“生产者-消费者”模型都忘了是什么了?好吧,我们还是先来看下回顾一下“生产者-消费者”模型吧。
1.“生产者-消费者”模型
“生产者-消费者”模型是一个典型的线程协作通信的例子。在这一模型中有两类角色,即若干个生产者线程和若干个消费者线程。生产者线程负责提交用户请求,消费者线程负责处理生产者提交的请求。很多情况下,生产者与消费者不能够达到一定的平衡,即有时候生产者生产的速度过快,消费之来不及消费;而有时候可能是消费者过于旺盛,生产者来不及生产。在此情况下就需要一个生产者与消费者共享的内存缓存区来平衡二者的协作。生产者与消费者之间通过共享内存缓存区进行通信,从而平衡生产者与消费者线程,并将生产者和消费者解耦。如下图所示:
当队列容器中没有商品的时候,就需要让消费者处于等待状态,而当容器满了之后就需要生产者处于等待状态。而消费者每消费一个商品,又会通知正在等待的生产者可以进行生产了;当生产则生产一个商品,也会通知正在等待的消费者可以消费了。
2.使用synchronized实现“生产者-消费者”模型
了解了“生产者-消费者”模型后,我们尝试使用synchronized关键字结合wait()与notifyAll()方法来实现一个”生产者-消费者“模型的例子。
我们选一个比较经典的生产面包的例子来看,首先需要一个面包容器类,容器类中有放入面包和取出面包两个操作,代码如下:
public class BreadContainer {
LinkedList<Bread> list = new LinkedList<>();
// 容器容量
private final static int CAPACITY = 10;
/**
* 放入面包
*/
public synchronized void put(Bread bread) {
while (list.size() == CAPACITY) {
try {
// 如果容器已满,则阻塞生产者线程
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.add(bread);
// 面包生产成功后通知消费者线程
notifyAll();
System.out.println(Thread.