一:单例模式
🔺注:单例模式是校招中最常考的设计模式之一
▲设计模式:设计模式好比象棋中的 "棋谱"。红方当头炮,黑方马来跳。针对红方的一些走法,,黑方应招的时候有 一些固定的套路。 按照套路来走局势就不会吃亏
软件开发中也有很多常见的 "问题场景"。针对这些问题场景, 大佬们总结出了一些固定的套路。按照 这个套路来实现代码, 也不会吃亏
💗单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例
💜简单理解就是只能被new一次
单例模式具体的实现方式, 分成 "饿汉模式" 和 "懒汉模式" 两种
重点在于懒汉模式!!!
(1)饿汉模式
①特点
⧎类加载的同时, 创建实例
⯭只能被new一次
💚一般称类为Singleton
②代码
// 单例模式(饿汉模式)
class Singleton {
//类加载的同时, 创建实例
private static Singleton instance = new Singleton();
public static Singleton getInstance() { //创造一个静态方法getInstance,作用是返回对象
return instance;
}
// 做出一个限制,将构造方法封装加上private,禁止别人去 new 一个实例!
private Singleton() {}
}
public class Demo19 {
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
// Singleton s3 = new Singleton(); //这条代码会报错;上面已经将构造方法封装加上private了
System.out.println(s1 == s2); //true
}
}
③关于线程安全问题
🌟饿汉模式并不会出现线程安全问题!!!
💖原因:
这个getInstance静态方法的作用就是返回instance,本质操作就是在读取变量
从上一篇文章Java线程②中我们可以得知多个线程读取同一个变量是可行的
换句话说,多线程读取同一变量不会引发线程安全问题!!!
(2)懒汉模式-普通版
①特点
⧎类加载的时候不创建实例. 第一次使用的时候才创建实例(能不创建就不创建)
⯭只能被new一次
💚一般称类为SingletonLazy
②代码
class SingletonLazy {
//类加载的时候不创建实例. 第一次使用的时候才创建实例.
private static SingletonLazy instance = null; //类加载时设置为空
//创造一个静态方法getInstance,作用是判断如果是第一次使用就创建实例
public static SingletonLazy getInstance() {
if (instance==null){ //如果为null就是没有实例化过就进去实例化
instance = new SingletonLazy();
}
return instance; //不为null也就是实例化了,返回即可
}
// 做出一个限制,将构造方法封装加上private,禁止别人去 new 一个实例!
private SingletonLazy() {}
}
public class Demo20 {
public static void main(String[] args) {
//s1调用时,因为是第一次使用instance为null,所以进入if创建实例
SingletonLazy s1 = SingletonLazy.getInstance();
//s2调用时,因为已经创建过实例了,此时instance不为null,所以直接返回即可
SingletonLazy s2 = SingletonLazy.getInstance();
System.out.println(s1==s2);
// SingletonLazy s3 = new SingletonLazy(); //这条代码会报错;上面已经将构造方法封装加上private了
}
}
③关于线程安全问题-普通版
🌟懒汉模式-普通版是会出现线程安全问题!!!
⭐原因:与原子性、内存可见性以及指令重排序有关
instance=new SingletonLazy();这条代码是搭配if条件语句来使用的,分成了两部分代码,就不是原子的,有可能会出现线程安全问题!
假设我们现在有t1、t2两个线程去调用getInstance,t1先判断,t2没等t1实例化就判断了,此时t2中就认为instance是null,然后t1实例化,t2中instance为null,就又实例化一次,最终我们就能看到实例化new了两次,此时就会出现线程安全问题!
(3)懒汉模式-加锁版
①特点
⧎类加载的时候不创建实例. 第一次使用的时候才创建实例
⯭只能被new一次
⚝用synchronized给方法加锁
💚一般称类为SingletonLazy
②代码
class SingletonLazy {
//类加载的时候不创建实例. 第一次使用的时候才创建实例.
private static SingletonLazy instance = null; //类加载时设置为空
//创造一个静态方法getInstance,作用是判断如果是第一次使用就创建实例
public static SingletonLazy getInstance() {
//synchronized修饰静态代码块(上一篇文章Java线程②有提及)
//给整个if条件加锁,让整段代码作为一个原子不可分割
synchronized (SingletonLazy.class) {
if (instance == null) { //如果为null就是没有实例化过就进去实例化
instance = new SingletonLazy();
}
}
return instance; //不为null也就是实例化了,返回即可
}
// 做出一个限制,将构造方法封装加上private,禁止别人去 new 一个实例!
private SingletonLazy() {}
}
public class Demo21 {
public static void main(String[] args) {
//s1调用时,因为是第一次使用instance为null,所以进入if创建实例
SingletonLazy s1 = SingletonLazy.getInstance();
//s2调用时,因为已经创建过实例了,此时instance不为null,所以直接返回即可
SingletonLazy s2 = SingletonLazy.getInstance();
System.out.println(s1==s2);
// SingletonLazy s3 = new SingletonLazy(); //这条代码会报错;上面已经将构造方法封装加上private了
}
}
③关于线程安全问题-加锁版
🌟懒汉模式-加锁版仍然还会出现线程安全问题!!!
🔯虽然解决了原子性,但是还会出现线程安全问题,除此之外还会出现效率低下问题!!!
1.无脑加锁导致效率低下
⭐原因:使用了synchronized就代表了以后每次调用getInstance都会加锁,但是这种加锁是不必要的;首次调用加锁,很正常,可第二次也加锁,就没啥必要了,会降低效率,因为线程不安全原因主要是首次new对象的时候才存在问题,此时t2可能在t1没实例化之前就判断了,如果把对象new好了,后续再调用getInstance就都是return读操作而已!!!
(双层if解决)
2.内存可见性以及指令重排序导致线程安全问题
⭐原因:t1线程在修改为instance的值不为null后,就结束代码块然后释放锁,t2就能得到锁然后从阻塞中恢复,如果编译器进行优化,那么t2就一定能够读取到t1修改过的值吗?这里就会出现内存可见性问题!!!
(关键字volatile解决)
(4)懒汉模式-双层if加锁版
①特点
⧎类加载的时候不创建实例. 第一次使用的时候才创建实例
⯭只能被new一次
⚝用synchronized给方法加锁,且在synchronized外再加一层if判断
💚外层先判断是否要加锁,内层再判断是否要真正加锁进行实例化
②代码
class SingletonLazy {
//类加载的时候不创建实例. 第一次使用的时候才创建实例.
private static SingletonLazy instance = null; //类加载时设置为空
public static SingletonLazy getInstance() {
//外层if进行判断,如果instance == null则是首次调用需要加锁,如果是非null就说明是后续调用,从而不再进行加锁
if(instance == null){
synchronized (SingletonLazy.class) {
if (instance == null) { //如果为null就是没有实例化过就进去实例化
instance = new SingletonLazy();
}
}
}
return instance; //不为null也就是实例化了,返回即可
}
// 做出一个限制,将构造方法封装加上private,禁止别人去 new 一个实例!
private SingletonLazy() {}
}
public class Demo22 {
public static void main(String[] args) {
//s1调用时,因为是第一次使用instance为null,所以进入if创建实例
SingletonLazy s1 = SingletonLazy.getInstance();
//s2调用时,因为已经创建过实例了,此时instance不为null,所以直接返回即可
SingletonLazy s2 = SingletonLazy.getInstance();
System.out.println(s1==s2);
// SingletonLazy s3 = new SingletonLazy(); //这条代码会报错;上面已经将构造方法封装加上private了
}
}
③关于线程安全问题-双层if加锁版
🌟懒汉模式-双层if加锁版仍然还会出现线程安全问题!!!
⭐原因:内存可见性以及指令重排序导致线程安全问题
内存可见性上面已经说过了,这里说说指令重排序的问题
指令重排序:我们知道指令重排序也是编译器优化带来的“好心办坏事”;在保证原有逻辑执行不变的前提下,对代码执行顺序进行调整,使调整之后执行效率提高;但是对于多线程来说,调整执行顺序可能会导致线程安全问题!!!
这段代码其实是分三步的
①给对象创造出内存空间,得到内存地址
②在空间上调用构造方法,给对象进行初始化
③把内存地址赋值给instance引用变量
✹如果在多线程中,这几个顺序如果发生变化就会导致线程安全问题
例:假设由于指令重排序,编译器的优化,令这条代码是按照①③②顺序执行,第一个线程的操作就是先给对象创建内存空间,把地址给instance,此时已经执行了①③;但是如果这时发生了线程转换,转给了第二个线程,可是我的第一个线程还没来得及初始化呢,那么第二个线程在进行判断if(instance == null)的时候,就会因为前面第一个线程已经创建出实例而判断不为空,于是直接返回第一个线程创建的对象instance,并且后续可能用到instance的方法和属性。那么问题来了,此处第二个线程拿到的是一个不完整的对象,因为第一个线程还没有初始化。拿到一个不完整的对象,后续出现什么问题很难保证,作为程序猿我们也不想发生这种事情吧??!!
(用关键字volatile解决,此时的代码就必须按指定顺序进行执行)
(5)懒汉模式-正确版
①特点
⧎类加载的时候不创建实例. 第一次使用的时候才创建实例
⯭只能被new一次
⚝用synchronized给方法加锁,且在synchronized外再加一层if判断
❄给instance加上volatile
💚外层先判断是否要加锁,内层再判断是否要真正加锁进行实例化
💚给instance加上volatile彻底解决所有问题
②代码
class SingletonLazy {
//类加载的时候不创建实例. 第一次使用的时候才创建实例
//给instance加上volatile
private static volatile SingletonLazy instance = null; //类加载时设置为空
public static SingletonLazy getInstance() {
//外层if进行判断,如果instance == null则是首次调用需要加锁,如果是非null就说明是后续调用,从而不再进行加锁
if(instance == null){
synchronized (SingletonLazy.class) {
if (instance == null) { //如果为null就是没有实例化过就进去实例化
instance = new SingletonLazy();
}
}
}
return instance; //不为null也就是实例化了,返回即可
}
// 做出一个限制,将构造方法封装加上private,禁止别人去 new 一个实例!
private SingletonLazy() {}
}
public class Demo22 {
public static void main(String[] args) {
//s1调用时,因为是第一次使用instance为null,所以进入if创建实例
SingletonLazy s1 = SingletonLazy.getInstance();
//s2调用时,因为已经创建过实例了,此时instance不为null,所以直接返回即可
SingletonLazy s2 = SingletonLazy.getInstance();
System.out.println(s1==s2);
// SingletonLazy s3 = new SingletonLazy(); //这条代码会报错;上面已经将构造方法封装加上private了
}
}
③关于线程安全问题-正确版
同时拥有synchronized、双层if、volatile的懒汉模式才是正确的!!!
但是给面试官写的时候,不要一开始就写正确版本的,可以先写普通版本➔加锁版➔双层if加锁版➔正确版,主打的就是一个欲擒故纵,不但让面试官有台阶下,你也能给他一个很好的印象!!!
二:阻塞式队列
(1)概念
阻塞队列是一种特殊的队列. 也遵守 "先进先出" 的原则
(2)特点
阻塞队列能是一种线程安全的数据结构
当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素
当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素
(3)应用场景
生产者消费者模型
(4)生产者消费者模型
1.概念
一种非常典型的开发模型,也是处理多线程问题的方式!!!
2.优势
①解耦合
💗降低模块之间的耦合,也就是降低模块之间的影响
比如过年一家人一起包饺子
一般都是有明确分工,比如一个人负责擀饺子皮,其他人负责包
擀饺子皮的人就是 "生产者",包饺子的人就是 "消费者"
擀饺子皮的人不关心包饺子的人是谁(能包就行, 无论是手工包, 借助工具, 还是机器包)
包饺子的人也不关心擀饺子皮的人是谁(有饺子皮就行, 无论是用擀面杖擀的, 还是拿罐头瓶擀, 还是直接从超市买的)
例如:A是直接把请求发给B,由B去完成一系列操作,此时A和B之间的耦合就比较明显!数据传输是双向的,假设B挂了就可能对A造成很大影响,会造成A给B发送请求那个接口很容易抛出异常,且异常要是没处理好A也就跟着挂了;反之,B要给A返回响应数据(比如充值是否成功,现余额是多少等等),假设A挂了就可能对B造成很大影响,会造成B给A返回响应那个接口抛出异常!如果要新增一个服务器C,此时A服务器的代码要做出很大的修改!!!
引入生产者消费者模型和阻塞队列就能有效解决上述问题:
缺点:效率降低;因为之前是A直接通信给B的,是一次通信!而现在A要先发送给队列,再从队列发给B,这就是两次通信!
此时A与B之间不是直接通信,而是都把请求写进一个阻塞队列中,通过阻塞队列,A和B就能很好的解耦合了。假设A或者B挂了,由于他们彼此之间不会直接交互,没有啥太大影响,不会出现一个挂了把另外一个也顺着挂了。比如A挂了,B并不是直接跟A交互,而是跟队列交互,B根本就不知道A的存在,而队列又工作的好好的,不会影响到B!如果要新增一个服务器C,此时A服务器不需要任何修改,只要让C从队列中取元素即可!!!甚至A都不知道C存在,你乐意有个DEFG都行,爱咋咋地!!!
②削峰填谷
💗平衡了生产者和消费者的处理能力,防止服务器被突然到来的一波请求直接冲垮
服务器收到客户端/用户的请求不是一成不变的,可能会因为一些突发事件,导致请求数暴增
一台服务器的同一时刻能处理的请求是有上限的,且不同服务器的处理请求上限不一样
是因为机器的硬件资源是有限的,服务器每处理一次请求,都需要消耗一定的硬件资源
例如:
引入生产者消费者模型和阻塞队列就能有效解决上述问题:
(5)标准库中的阻塞队列
💚在 Java 标准库中内置了阻塞队列,可直接使用标准库里的
①BlockingQueue 是一个接口,真正实现的类是 LinkedBlockingQueue
②put 方法用于阻塞式的入队列,take 用于阻塞式的出队列
💛出队列,如果没有 put 或者队列为空直接 take,就会阻塞
③BlockingQueue 也有 offer, poll, peek 等方法,但是这些方法不带有阻塞特性
④三种实现方式
import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; //使用一下Java 标准库中内置的阻塞队列 public class Demo23 { public static void main(String[] args) throws InterruptedException { BlockingQueue<String> queue = new LinkedBlockingQueue<>(10); //代表最大容量是10个元素 //put方法入队列 queue.put("hlizoo"); //take方法出队列 String elem = queue.take(); System.out.println(elem); //上面已经取出过元素了,此时队列为空,如果这里再次取一次,会阻塞!!! elem = queue.take(); } }
(6)生产者消费者模型代码
// 生产者消费者模型
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class Demo24 {
public static void main(String[] args) {
// 搞一个阻塞队列, 作为交易场所
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
// t1负责生产元素
Thread t1 = new Thread(() -> {
int count = 0;
while (true) {
try {
queue.put(count); //放入元素
System.out.println("生产元素: " + count);
count++;
Thread.sleep(1000); //限制节奏;每次入队之后隔1s再入队
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// t2负责消费元素
Thread t2 = new Thread(() -> {
while (true) {
try {
Integer n = queue.take(); //取出元素
System.out.println("消费元素: " + n); //虽然没有sleep,但是节奏跟着生产者t1走
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start(); //启动t1线程
t2.start(); //启动t2线程
}
}
(7)模拟实现阻塞队列
💛通过 "循环队列" 的方式来实现
💜使用 synchronized 进行加锁控制
💜使用 volatile保证内存可见性和指令重排序
💓put 插入元素的时候, 判定如果队列满了, 就进行 wait
(注意, 要在循环中进行 wait)
原因:如果用了if,假设put操作因为队列满了而进入wait阻塞,过了一会wait被唤醒了,那么当被唤醒的时候此时的队列就一定不满了吗?是否还存在着队列仍然满的可能性?万一出现了队列还是满的,此时就意味着如果接下来的代码继续执行,就可能把之前的元素给覆盖了;当用了while循环,wait被唤醒后就继续判断条件嘛,如果条件还是size >= items.length队列满了,那就继续wait,否则,就跳出循环往下执行
💓take 取出元素的时候, 判定如果队列为空, 就进行 wait
(注意, 要在循环中进行 wait)
原因同上
class MyBlockingQueue {
//队列是 队尾进 队头出,满足先进先出原则!!!
// 使用一个 String 类型的数组items来保存元素. 假设这里只存 String
private String[] items = new String[1000];
// 当 head 和 tail 相等(重合), 相当于空的队列
// 指向队列的头部
volatile int head = 0;
// 指向队列的尾部的下一个元素. 总的来说, 队列中有效元素的范围 [head, tail)
volatile private int tail = 0;
// 使用 size 来表示元素个数.(用来表示当size=items.length时队列已满)
volatile private int size = 0;
//定义一个锁对象
private Object locker = new Object();
// 入队列
public void put(String elem) throws InterruptedException {
// 此处的写法就相当于直接把 synchronized 写到方法上了
synchronized (locker) {
while (size >= items.length) {
//数组元素个数大于等于数组长度就表示元素已满
// 队列满了,阻塞等待!
locker.wait();
}
//元素没满就添加元素入队列
items[tail] = elem;
//元素添加完后tail记得往后走
tail++;
//判断tail如果走到了末尾就回到0下标
if (tail >= items.length) {
tail = 0;
}
//添加,元素个数加1
size++;
// 用来唤醒队列为空的阻塞情况
locker.notify();
}
}
// 出队列
public String take() throws InterruptedException {
synchronized (locker) {
while (size == 0) {
//数组元素个数为0就代表队列为空
// 队列为空, 暂时不能出队列,阻塞等待!
locker.wait();
}
//队列不为空就取出元素放elem
String elem = items[head];
//取完元素记得head往后走
head++;
//判断head如果走到了末尾就回到0下标
if (head >= items.length) {
head = 0;
}
//取出,元素个数减1
size--;
// 使用这个 notify 来唤醒队列满的阻塞情况
locker.notify();
return elem;
}
}
}
public class Demo25 {
public static void main(String[] args) throws InterruptedException {
// 创建两个线程, 表示生产者和消费者
MyBlockingQueue queue = new MyBlockingQueue();
Thread t1 = new Thread(() -> {
int count = 0;
while (true) {
try {
queue.put(count + "");
System.out.println("生产元素: " + count);
count++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(() -> {
while (true) {
try {
String count = queue.take();
System.out.println("消费元素: " + count);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
}
}
三:定时器
(1)概念
定时器也是软件开发中的一个重要组件,类似于一个 "闹钟"!
达到一个设定的时间之后,就执行某个指定好的代码!
(2)标准库中的定时器
Timer timer = new Timer(); //Timer类的实例化 timer.schedule(new TimerTask() { //timer中的具体方法schedule(与runnable类似用法) @Override public void run() { //具体要干什么 } },x); //x ms秒后执行
标准库中提供了一个 Timer 类
💘Timer 类的核心方法为 schedule
schedule方法中包含两个参数
💗第一个参数new TimerTask()指定即将要执行的任务代码
💗第二个参数指定多长时间之后执行 (单位为毫秒)
💚new TimerTask()里面重写的run方法就表示定时器到指定时间后要干什么
💕 TimerTask()与runnable()类似,主要原因是实现了Runnable接口
💕 TimerTask()实现了Runnable接口,run方法也就是到点了需要执行什么代码
①代码
import java.util.Timer;
import java.util.TimerTask;
//定时器的使用
public class Demo26 {
public static void main(String[] args) {
Timer timer = new Timer();
//定义三个定时器
timer.schedule(new TimerTask() {
@Override
public void run() { //定时器需要干什么事
System.out.println("Hello MyAlarm3!!!");
}
},3000); //3s之后开始
timer.schedule(new TimerTask() {
@Override
public void run() { //定时器需要干什么事
System.out.println("Hello MyAlarm2!!!");
}
},2000); //2s之后开始
timer.schedule(new TimerTask() {
@Override
public void run() { //定时器需要干什么事
System.out.println("Hello MyAlarm1!!!");
}
},1000); //1s之后开始
}
}
②执行效果
(3)模拟实现定时器
①创建一个MyTimerTask类, 用来描述定时器中的某一个任务,这个任务包括两个方面,一个是任务的内容,一个是任务的实际执行时间
✹因为优先级队列,所以需要实现 Comparable 接口
②定义一个类叫MyTimer作为定时器类的本体
用优先级队列把多个任务(MyTimerTask)组织起来
◬原因:使用优先级队列在于我们只需要盯住时间最靠前的任务即可,最早执行的任务时间还没到,则其他任务时间肯定没到,而优先级队列的队首元素就是时间最小的
③再在MyTimer类中创建一个构造方法,里面创建一个线程作为 "扫描线程", 一方面去负责监控队首元素是否到点了, 是否应该执行; 一方面当任务到点之后,用run方法去执行任务
import java.util.PriorityQueue;
// 创建一个MyTimerTask类, 用来描述定时器中的某一个任务!!!
class MyTimerTask implements Comparable<MyTimerTask> {
// 任务实际具体啥时候执行. 毫秒级的时间戳
private long time;
// 任务具体是啥,根据Runnable中的run方法去实现
private Runnable runnable;
public MyTimerTask(Runnable runnable, long delay) {
// delay 是一个相对的时间差,表示多久之后执行任务
// time作为实际执行时间,time=系统时间+delay
time = System.currentTimeMillis() + delay;
//runnable表示具体要干什么
this.runnable = runnable;
}
//获取一下执行时间
public long getTime() {
return time;
}
//获取一下runnable再调用run方法实现具体任务
public Runnable getRunnable() {
return runnable;
}
@Override
public int compareTo(MyTimerTask o) {
// 认为时间小的, 优先级高
// 最终时间最小的元素, 就会放到队首.
return (int) (this.time - o.time);
// return (int) (o.time - this.time); //表示时间大的优先级高
}
}
//创建一个MyTimer类,作为定时器类的本体,把多个任务<MyTimerTask>组织起来!!!
class MyTimer {
// 使用优先级队列, 来保存上述<MyTimerTask>的 N 个任务
private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
// 用来加锁的对象locker
private Object locker = new Object();
// 定时器的核心方法, 就是把要执行的新的任务添加到队列中.
public void schedule(Runnable runnable, long delay) {
synchronized (locker) {
//初始化任务task
MyTimerTask task = new MyTimerTask(runnable, delay);
//将task任务添加到队列(元素)
queue.offer(task);
// 每次来新的任务, 都唤醒一下之前的扫描线程
// 好让扫描线程根据最新的任务情况, 重新规划等待时间.
locker.notify();
}
}
// MyTimer 中还需要构造一个 "扫描线程", 一方面去负责监控队首元素是否到点了, 是否应该执行; 一方面当任务到点之后,
// 就要调用这里的 Runnable 的 Run 方法来完成任务
public MyTimer() {
// 扫描线程
Thread t = new Thread(() -> {
while (true) {
try {
synchronized (locker) {
while (queue.isEmpty()) {
// 注意, 当前如果队列为空, 此时就不应该去取这里的元素.
// 此处使用 wait 等待更合适. 如果使用 continue, 就会使这个线程 while 循环运行的飞快,
// 也会陷入一个高频占用 cpu 的状态(忙等).
locker.wait();
}
//task获取一下优先级最高的元素,也就是最先执行的任务
MyTimerTask task = queue.peek();
//获取当前时间
long curTime = System.currentTimeMillis();
if (curTime >= task.getTime()) {
// 假设当前时间是 14:01, 任务时间是 14:00, 此时就意味着应该要执行这个任务了.
//上述task已经获得最先执行的任务,后面马上就要执行了
//执行完任务这个任务肯定就要删除了,这里就移除优先级最高也就是已完成的元素
queue.poll();
//调用run方法开始执行任务
task.getRunnable().run();
} else {
// 让当前扫描线程休眠一下, 按照时间差来进行休眠,看看离任务执行还差多久.
// Thread.sleep(task.getTime() - curTime);
locker.wait(task.getTime() - curTime);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
四:线程池
(1)概念和作用
概念:
将一些要释放的资源,不要着急释放,而是先放到一个“池子”里,以备后续使用
申请资源的时候,也先提前把申请的资源申请好,也放到一个“池子”里,后续申请时也方便
作用:减少每次启动、销毁线程的损耗
问题:那么为什么从池里取线程就比从系统创建线程更加高效呢?
回答:
①如果从系统这里创建线程,需要调用系统API,进一步的由操作系统内核去完成线程创建的过程,那么,这里就是内核给所有进程提供服务,这种由内核创建的过程是不可控的,因为它可能在创建的过程中会去执行别的事!
②如果是从线程池里面获取线程,上述在内核中进行的操作就都提前做好了,取线程的过程中,纯粹靠的是用户代码完成即可,也即是纯用户态,这种是可控的,用户想立刻创建就用代码立刻创建!
(2)标准库中的线程池
①使用ExecutorService类
②然后Executors.静态方法创建线程池
Executors 本质上是 ThreadPoolExecutor 类的封装
💛以下都是属于Executors的静态方法
newFixedThreadPool()
newCachedThreadPool()
newSingleThreadExecutor()
newScheduledThreadPool()
![]()
③创建好线程池之后,使用submit方法就可将任务添加到线程池里
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
// 线程池
public class Demo28 {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(4); //创建出固定包含 4 个线程的线程池
for (int i = 0; i < 10; i++) {
service.submit(new Runnable() { //调用submit方法添加任务
@Override
public void run() { //在run方法里面执行
System.out.println("hello"); //任务就是打印hello
}
});
}
}
}
(3)ThreadPoolExecutor
💜ThreadPoolExecutor是一个接口更丰富的线程池类,提供了更多的可选参数
💙ThreadPoolExecutor里面的线程数会根据当前任务情况动态变化(自适应)
⭐从图里我们可以看到,ThreadPoolExecutor提供了七个可选参数
①int corePoolSize:核心线程数
(表示最少得包含这些线程数目,哪怕线程池里没有任务)
②int maximumPoolSize:最大线程数
(最多不能超过这些线程数,哪怕你的线程池已经忙到冒烟了,也不能比这个数目多)
③long keepAliveTime:最大空闲时间
TimeUnit unit:时间单位
(当线程空闲超过了指定时间,就可以销毁了)
④BlockingQueue<Runnable> workQueue:线程池里的任务可以用阻塞队列来管理
线程池可以内置阻塞队列,也可以手动指定一个
⑤ThreadFactory threadFactory:工厂模式;通过这个工厂类创建线程
⑥RejectedExecutionHandler handler:拒绝方式/拒绝策略
(线程池里的阻塞队列,当阻塞队列满了之后,继续添加新任务时该如何应对?)
💓三种拒绝策略:
①直接抛出异常,线程池放弃工作,不管什么任务都不做,摆烂了
②哪个线程添加了这个新任务,这个线程就去执行这个任务
③放弃最早的任务,然后执行新的任务
④放弃最新的任务,继续执行之前的任务
(4)模拟实现线程池
使用一个 BlockingQueue 组织所有的任务
核心操作为 submit, 将任务加入线程池中
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
class MyThreadPool {
//实现一个阻塞式队列来管理线程池里的任务
private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
// 通过submit方法, 来把任务添加到线程池中.
public void submit(Runnable runnable) throws InterruptedException {
queue.put(runnable);
}
// n 表示线程池里有几个线程.
// 创建了一个固定数量的线程池.
public MyThreadPool(int n) {
for (int i = 0; i < n; i++) {
Thread t = new Thread(() -> {
while (true) {
try {
// 取出任务, 并执行~~
Runnable runnable = queue.take();
runnable.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
}