第一章 线程的等待和通知
第01节 方法引入
Object类中的方法
- wait() 让当前线程进入等待状态,直到被通知为止
- wait(long) 让当前线程进入等待状态,同时设置时间;直到被通知为止或时间结束
- notify() 随机通知一个等待线程
- notifyAll() 通知所有的等待线程
注意:等待和通知方法必须是锁对象,否则会抛出IllegalMonitorStateException
第02节 案例代码
/**
* 通过锁对象将线程等待,经过5秒通知该线程来执行
*/
public class WaitDemo {
public synchronized void print() throws InterruptedException {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "--->" +i);
if(i == 50){
//让当前线程等待
this.wait();
}
}
}
public synchronized void notifyTest(){
//让等待的线程执行
this.notifyAll();
}
public static void main(String[] args) {
WaitDemo waitDemo = new WaitDemo();
new Thread(()->{
try {
waitDemo.print();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
waitDemo.notifyTest();
}
}
第03节 wait和sleep的区别
-
调用对象不同
wait() 由锁对象调用
sleep() 由线程调用
-
锁使用不同
执行wait后,自动释放锁
执行sleep后,不会自动释放锁
-
唤醒机制不同
执行wait后,可以被通知唤醒
执行sleep后,只能等待时间结束后,自动唤醒
第二章 生产者消费者模式
第01节 概述
一种设计模式,不属于GOF23
-
生产者
某些程序/进程/线程负责生产数据就属于生产者
-
消费者
某些程序/进程/线程负责使用数据就属于消费者
第02节 生产者和消费者之间的问题
- 问题引入
- 耦合性高,生产者和消费者联系紧密,不利于系统的扩展和维护
- 并发性能低,同时能处理请求量少
- 忙闲不均,生产者和消费者的速度不一致,带来系统资源的浪费
- 解决方式
因此我们可以设置一个数据缓冲区,让生产者生产的数据存放再缓冲区,然 后让消费者在缓冲区来取的数据,这样就降低了生产者和消费者的耦合性。其次我们还可以给缓冲区设置一个固定的最大容量值,当数据达到最大值生产者进入等待,等缓冲区有空余位置的时候再唤醒生产者。同样的当缓冲区数据为空的时候,消费者进入等待,等缓冲区数据不为空的时候再唤醒消费者线程,这样就可以解决生产者和消费者的速度不一致,带来系统资源的浪费的问题。
第03节 案例代码
程序模拟一个包子铺,一个生产者线程生产100个包子,10个消费者线程并且每个消费者线程消费十个包子。
import java.util.ArrayList;
import java.util.List;
/**
* 包子铺
*/
public class BaoziShop {
/**
* 包子
*/
class Baozi{
private int id;
public Baozi(int id) {
this.id = id;
}
@Override
public String toString() {
return "包子--" + id;
}
}
//上限
public static final int MAX_COUNT = 100;
//缓冲区 存放数据
private List<Baozi> baozis = new ArrayList<>();
/**
* 做包子
*/
public synchronized void makeBaozi() throws InterruptedException {
//判断缓冲区是否满了
if(baozis.size() == MAX_COUNT){
System.out.printf("缓冲区满了,%s等待%n",Thread.currentThread().getName());
//让生产者线程等待
this.wait();
}else{
//通知生产者线程生产
this.notifyAll();
}
//创建包子
Baozi baozi = new Baozi(baozis.size() + 1);
System.out.println(Thread.currentThread().getName()+"做了"+baozi);
//保存到缓冲区
baozis.add(baozi);
}
/**
* 拿包子
*/
public synchronized void takeBaozi() throws InterruptedException {
//判断缓冲区是否空了
if(baozis.size() == 0){
System.out.printf("缓冲区空了,%s等待%n", Thread.currentThread().getName());
//让消费者等待
this.wait();
}else{
//通知消费者消费
this.notifyAll();
}
//获得第一个包子,并删除
if(baozis.size() > 0){
Baozi baozi = baozis.remove(0);
System.out.println(Thread.currentThread().getName()+"吃了"+baozi);
}
}
public static void main(String[] args) {
BaoziShop baoziShop = new BaoziShop();
//一个生产者
Thread productor = new Thread(() -> {
for (int i = 0; i < 100; i++) {
try {
baoziShop.makeBaozi();
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
productor.start();
//10个消费者吃10个包子
for (int i = 0; i < 10; i++) {
Thread consumer = new Thread(() ->{
try {
for (int j = 0; j < 10; j++) {
baoziShop.takeBaozi();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
consumer.start();
}
}
}
应用场景:可以应用于各自生产资源和消费资源的案例之中
- 12306售票
- 消息队列
- 线程池
- 等等
第三章 阻塞队列
第01节 问题引入
什么是阻塞队列,为什么会有消息队列。我们通过上面的卖包子的案例可以发现一个问题,我们的缓冲区是我们创建的一个ArrayList,我们需要手动的编写代码逻辑来判断缓存区的数据是否满了是否空了。这样以来是十分的繁琐的。有没有什么缓存容器可以帮我们解决这个问题,能够根据数据满或空的情况,自动对线程执行等待和通知。这样以来就出现了我们阻塞队列这个概念。
第02节 快速入门
BlockingQueue 接口
- put 添加数据,达到上限会自动让线程等待
- take 取并删除数据,数据空了会自动让线程等待
实现类
ArrayBlockingQueue 类 数据结构为数组
LinkedBlockingQueue类 链表结构
第03节 案例代码
刚刚我们分析了传统的方式通过ArrayList作为缓冲区容器的痛点,现在我们就可以用阻塞队列来帮我们解决此痛点。以下我们还是通过卖包子的案例来优化代码。
/**
* 包子铺
*/
public class BaoziShop2 {
/**
* 包子
*/
static class Baozi{
private int id;
public Baozi(int id) {
this.id = id;
}
@Override
public String toString() {
return "包子--" + id;
}
}
public static void main(String[] args) {
//阻塞队列
BlockingQueue<Baozi> baozis = new ArrayBlockingQueue<>(100);
//生产者线程
new Thread(() -> {
for (int i = 0; i < 200; i++) {
//创建包子,添加到阻塞队列,满了就自动阻塞线程
Baozi baozi = new Baozi(baozis.size() + 1);
try {
baozis.put(baozi);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"生产了"+baozi);
}
}).start();
//消费者线程
for(int i = 0;i < 5;i++){
new Thread(() -> {
//取包子,空了会自动阻塞
for (int j = 0; j < 40; j++) {
try {
Baozi baozi = baozis.take();
System.out.println(Thread.currentThread().getName()+"消费了"+baozi);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
}
第四章 线程池
第01节 概念引入
为什么我们会有线程池的这个概念,因为线程是一种宝贵的系统资源,在之前的传统方式实现多线程,当我们的线程执行完它的全部指令那么他的生命周期就到了死亡状态。当我们再次需要执行任务的时候又需要重新来新建线程。如果有大量任务需要处理,需要频繁的创建和销毁线程,是对我们系统资源很大的浪费。因此有了线程池来帮我们解决这种浪费。
线程池的核心思想:
线程池会保存一定量的线程,线程执行完任务后,会回到线程池中,等待下一个任务,节省系统资源,提升性能。简单的来说就是线程任务执行完了不会进入死亡状态了,而是进入线程池了。
第02节 线程池的使用
顶层接口:Executor
- execute(Runnable) 启动线程执行一个任务
ExecutorService
继承 Executor
添加线程池管理方法,如:shutdown()、shutdownNow()
Executors
用于创建线程池的工具类
主要的方法
方法名 | 说明 |
---|---|
newCachedThreadPool() | 创建长度不限的线程池 |
newFixedThreadPool(int ) | 创建固定长度的线程池 |
newSingleThreadExecutor() | 创建单一个数的线程池 |
newScheduledThreadPool(int) | 创建可以调度的线程池 |
第03节 案例代码
import java.time.LocalDateTime;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ThreadPoolDemo {
public static void useCachedThreadPool(){
//创建长度不限的线程池
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
int n = i;
//使用线程池启动线程
executorService.execute(()->{
System.out.println(Thread.currentThread().getName()+"执行了任务" + n);
});
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
executorService.shutdown();
}
public static void useFixedThreadPool(){
//创建长度固定的线程池
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 100; i++) {
int n = i;
//使用线程池启动线程
executorService.execute(()->{
System.out.println(Thread.currentThread().getName()+"执行了任务"+n);
});
}
executorService.shutdown();
}
public static void useSingleThreadPool(){
//创建单一长度的线程池
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 100; i++) {
int n = i;
//使用线程池启动线程
executorService.execute(()->{
System.out.println(Thread.currentThread().getName()+"执行了任务"+n);
});
}
executorService.shutdown();
}
public static void useScheduledThreadPool(){
//获得可调度的线程池
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
//执行可调度任务
System.out.println("------------");
scheduledExecutorService.scheduleAtFixedRate(()->{
System.out.println(Thread.currentThread().getName()+"---->"+ LocalDateTime.now());
},1,3, TimeUnit.SECONDS);
}
public static void main(String[] args) {
// useCachedThreadPool();
// useFixedThreadPool();
// useSingleThreadPool();
useScheduledThreadPool();
}
}
第五章 线程池的优化配置
第01节 概论简介
线程池的实现类
ThreadPoolExecutor
线程池的构造方法参数:
- corePoolSize 核心线程数,创建线程池后自带线程,不会进行销毁
- maximumPoolSize 最大线程数
- keepAliveTime 存活时间,非核心线程能够闲置的时间,超过后被销毁
- timeUnit 时间单位
- blockingQueue 阻塞队列 存放任务(Runnable)的集合
Executors工具类的部分源码
Tips:
通过源码我们会发现一个问题,实际上我们上面的Executors用于创建线程池的工具类,其实就是内部帮我们初始化好了不同ThreadPoolExecutor类的构造方法,通过搭配不同比例的构造方法参数的值来达到不同的线程池的功能。
优化配置
-
核心线程数 应该和CPU内核数量相关 CPU内核数 * N (N和任务执行需要时间和并发量相关)
// 查看CPU核心数的代码 Runtime.getRuntime().availableProcessors()
-
最大线程数可以和核心线程数一样,避免频繁创建和销毁线程
-
如果存在非核心线程,设置大一点,避免频繁创建和销毁线程
-
阻塞队列使用LinkedBlockingQueue,插入和删除任务效率更高
第六章 线程池的实现原理
学习了这么久,我们其实很想知道我们的线程池到底是如何实现的,为什么当我们一个线程执行完所有指令没有进入死亡的生命周期。我们来一起通过源码的方式来分析分析。
为什么我们的ThreadPoolExecutor类可以调用execute方法,通过以下的继承实现关系就可以明白了,其实最终我们的ThreadPoolExecutor类是和Executor接口产生了联系的。
我们来进入源码来看看execute方法执行线程池的时候到底内部发生了什么。
看到这里我们可以发现,每当我们执行线程池的时候,首先判断是否有传入一个实现Runnable接口的任务,如果没有则会抛出空指针异常。然后它会用我们的核心线程数和非核心线程的数量做比较,如果非核心线程数小于核心线程数,就创建核心线程。反之创建非核心线程。
我们来分析以下他的任务调度的机制,当有任务提交的时候,首先会判断线程池是否是还在运行的状态,如果不是则会任务拒绝。如果线程池还是运行状态我们才会继续判断线程数是否小于核心线程数,如果线程数<核心线程数我们可以理解为所有的核心线程都在执行任务,那我们就会添加一个工作线程来执行提交的任务。如果线程数大于等于核心线程数可理解为还有非核心的线程处于一个非工作状态,然后我们再判断任务的阻塞队列是不是满了,如果没有满,则直接把任务添加到阻塞队列等待工作线程来执行。如果阻塞队列已经满了,并且线程数已经是最大线程数了,则任务拒绝。如果线程数小于最大线程数则添加工作线程并执行。
然后跟进会循环调用getTask方法,如果任务队列为空则线程阻塞。直到阻塞队列有新的任务。
其实简单的来说。就是一个线程集合workerSet和一个阻塞队列workQueue。当用户向线程池提交一个任务task时,线程池会先将任务放入workQueue中。workerSet中的线程会不断的从workQueue中获取线程然后执行。当workQueue中没有任务的时候,worker就会阻塞,直到队列中有任务了就取出来继续执行。
今日总结
- 线程的等待和通知(是干什么,谁来调用)
线程的等待是通过锁对象来调用的,用来让当前线程进入等待状态,直到被通知为止。 - wait和sleep的区别
1.调用对象不同
wait() 由锁对象调用
sleep() 由线程调用
2.锁使用不同
执行wait后,自动释放锁
执行sleep后,不会释放锁
3.唤醒机制不同
执行wait后,可以被通知唤醒
执行sleep后,只能等待时间结束后,自动唤醒
-
生产者和消费者模式(是什么,解决什么问题,如何实现的,项目中有哪些应用)
是一种设计模式不属于GOF23,是某些进程/线程负责生成/消费数据。通过缓冲区实现。 -
什么是线程池
线程池会保存一定量的线程,线程执行完任务后,会回到线程池中,等待下一个任务,节省系统资源,提升性能。简单的来说就是线程任务执行完了不会进入死亡状态了,而是进入线程池了。