JAVAEE初阶第二节——多线程基础(下)
系列文章目录
JAVAEE初阶第二节——多线程基础(下)
多线程基础(下)
- 单例模式
- 阻塞式队列
- 定时器
- 线程池
文章目录
- 系列文章目录
- JAVAEE初阶第二节——多线程基础(下)
- 多线程基础(下)
- 一.多线程案例
- 1.单例模式
- 1.1 饿汉模式
- 1.2 懒汉模式
- 1.2.1 懒汉模式-单线程版
- 1.2.3 懒汉模式-多线程版
- 懒汉模式的改进(多线程)
- 2.阻塞式队列
- 2.1 阻塞队列的概念
- 2.2 生产者消费者模型
- 2.3标准库中的阻塞队列
- 2.4 阻塞队列的实现
- 3. 定时器
- 3.1 标准库中的定时器
- 3.2 实现定时器
- 4.线程池
- 4.1 标准库中的线程池
- 4.2 实现线程池
一.多线程案例
1.单例模式
单例模式是一种非常重要的设计模式之一
设计模式好比象棋中的 “棋谱”. 红方当头炮, 黑方马来跳. 针对红方的一些走法, 黑方应招的时候有一些固定的套路. 按照套路来走局势就不会吃亏.
软件开发中也有很多常见的 “问题场景”. 针对这些问题场景, 大佬们总结出了一些固定的套路. 按照这个套路来实现代码, 也不会吃亏.
单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例.
单例模式具体的实现方式, 分成 “饿汉” 和 “懒汉” 两种
1.1 饿汉模式
类加载的同时, 创建实例.
class Singleton{
private static Singleton instance = new Singleton();
public static Singleton getInstance(){
return instance;
}
private Singleton(){
}
}
上述代码,称为“饿汉模式”,是单例模式中一种简单的写法所谓"饿"形容"非常迫切”,是指实例是在类加载的时候就创建了,创建时机非常早,相当于程序一启动,实例就创建了。就使用"饿汉"形容"创建实例非常迫切,非常早"。
- 这个引用,就是期望创建出的唯一的实例的引用(static静态的指的是"类属性",instance就是Singleton类对象里面持有的属性)。每个类的类对象,只存在一个。类对象中的static属性,自然也是只有一个了。因此,instance指向的这个对象,就是唯一的一个对象!
private static Singleton instance = new Singleton();
- 其他代码要想使用这个类的实例就需要通过这个方法来进行获取。不应该在其他代码中重新new这个对象,而是使用getInstance()方法获取到现成的对象(已经创建好的唯一的对象,不是Thread)
- Singleton内部的代码早就把唯一的实例安排好了。
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1 == singleton2);
}
1.2 懒汉模式
懒汉模式与饿汉模式相比,创建实例的时机不太一样了创建实例的时机会更晚,只到第一次使用的时候,才会创建实例。社会能进步,科技能发展,效率生产力能提高,归根结底,都是因为"懒"。在计算机中,所谓的"懒”往往代表着"更高的效率"
1.2.1 懒汉模式-单线程版
class SingletonLazy{
private static SingletonLazy instance = null;
private SingletonLazy(){
}
public static SingletonLazy getInstance(){
if(instance == null){
instance = new SingletonLazy();
}
return instance;
}
}
如果是首次调用getlnstance,那么此时instance引用为null,就会进入if条件,从而把实例创建出来。
如果是后续再次调用getlnstance,由于instance已经不再是null此时不会进入if,直接返回之前创建好的引用了。
这样设定,仍然可以保证,该类的实例是唯一的一个。与此同时,创建实例的时机就不是程序驱动时了,而是第一次调用getlnstance的时候。(调用getlnstance的时间是未知的,什么时候调用(第一次),什么时候在创建实例。如果整个程序都被调用,就可以把创建实例的操作省去了。)
public static void main(String[] args) {
SingletonLazy singletonLazy1 = SingletonLazy.getInstance();
SingletonLazy singletonLazy2 = SingletonLazy.getInstance();
System.out.println(singletonLazy2 == singletonLazy1);
}
1.2.3 懒汉模式-多线程版
之前写的饿汉模式和懒汉模式在多线程环境下是否线程安全呢?
对于饿汉模式来说:getInstance直接返回的是instance实例,这个操作的本质是“读操作”,
而多线程读取同一个变量,是线程安全的
对于懒汉模式来说:因为懒汉模式中有读操作,也有写操作。所以如果两个线程按照下面的顺序执行就会出现一些问题
thread1先执行,if判断成功,进入if语句后,thread1停止。thread2开始执行,此时还没有创建实例于是进入if语句并创建一个实例。thread2结束,thread1继续刚才的操作,根据上下文得知还没有创建实例(此时已经有了一个实例),于是就创建一个实例。这样就会导致最终这两个线程创建了两个不同的实例,这就违反了单例模式的规则,是有问题的。
懒汉模式的改进(多线程)
加锁将if判定和new打包成“原子”的
class SingLetonLazy {
private static SingLetonLazy instance = null;
private static Object locker = new Object();
private SingLetonLazy(){
}
public static SingLetonLazy getInstance(){
synchronized(locker){
if(instance == null){
instance = new SingLetonLazy();
}
}
return instance;
}
}
此时就可以保证一定是thread1执行完new操作,执行完修改Instance之后,再回到thread2执行f,thread2的if条件就不会成立了。t2就会直接返回了。
但是上面的代码还是有一点问题,如果Instance已经创建过了。此时后续再调用getlnstance就都是直接返回Instance实例了。(此处的操作就是纯粹的读操作了,也就不会有线程安全问题了)此时,针对这个已经没有线程安全问题的代码,仍然是每次调用都先加锁,再解锁,此时,效率就非常低了。(加锁就意味着可能会产生阻塞)
所以要在需要加锁的时候才加锁,不该加锁,不能随便乱加。
这时候就可以在加锁外头再套上一层if,判定一下这个代码是否需要加锁。如果需要加锁,才加。如果不需要加锁,就不加锁。
class SingLetonLazy {
private static SingLetonLazy instance = null;
private static Object locker = new Object();
private SingLetonLazy(){
}
public static SingLetonLazy getInstance(){
if(instance == null){ //判断是否要加锁
synchronized(locker){
if(instance == null){ //判断是否要创建对象
instance = new SingLetonLazy();
}
}
}
return instance;
}
}
上面代码中的第一个if判定的是否要加锁,第二个if判定的是是否要创建对象(这两个if的判断条件是相同的)
指令重排序引起线程安全问题
上面代码还是有点问题,因为指令重排序引起的线程安全问题
指令重排序,也是编译器优化的一种方式。即调整原有代码的执行顺序,保证逻辑不变的前提下,提高程序的效率。
instance = new SingLetonLazy();
上面这段代码其实还可以拆成三个大的步骤
步骤1:申请一段空间
步骤2:在这个内存上调用构造方法,创建出这个实例
步骤3:把这个内存地址赋值给Instance引用变量
会引起线程安全问题的分析:
thread1会按照步骤1->步骤3->步骤2来执行这里的new操作。
当thread1的instance = new SingLetonLazy(); 执行到步骤3时(被优化成1,3,2的执行顺序),thread2开始执行,因为此时已经instance有一个未初始化对象的引用,所以thread2没有进入if触发加锁,也没有阻塞就直接返回了这个instance。此时所以instance里面的属性或者方法就有可能出现错误。(此时的instance的属性是一个未初始化的值)
过程总结:上面代码执行过程由于thread1线程执行完步骤1和步骤3后被调度走,此时的instance指向的是一个不为nul的值(未初始化对象)。此时thread2线程判断instance == null就不处理,直接返回。如果thread2继续使用instance里面的属性或者方法,就会出现问题(因为此时的属性都是未初始化的“全0”值),引起代码的逻辑出现问题。
要想解决这个问题,就可以使用volatile(保证内存可见性,禁止指令重排序)
其他地方不变
private volatile static SingLetonLazy instance = null;
2.阻塞式队列
2.1 阻塞队列的概念
阻塞队列是一种特殊的队列. 也遵守 “先进先出” 的原则.
(1)阻塞队列是一种线程安全的数据结构。
(2)具有阻塞特性:
- 如果针对一个已经满了的队列进行入队列,此时入队列操作就会阻塞。一直阻塞到队列不满(其他线程出队列元素)之后。
- 如果针对一个已经空了的队列进行出队列,此时出队列操作就会阻塞。一直阻塞到队列不空(其他线程入队列元素)之后
阻塞队列的一个典型应用场景就是 “生产者消费者模型”. 这是一种非常典型的开发模型.
2.2 生产者消费者模型
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。
生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取.
生产者消费者模型的作用:
- 阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力
- 阻塞队列也能使生产者和消费者之间解耦.
把代码的耦合程度,从高降低,就称为解耦合。
- 引入生产者消费者模型,就可以更好的做到"解耦合"
实际开发中,经常会涉及到"分布式系统”。(服务器整个功能不是由一个服务器全部完成的。而是每个服务器负责一部分功能通过服务器之间的网络通信,最终完成整个功能)
上面的模型中,A和B,A和C之间的耦合性就是比较强的。(A/B的代码中涉及到和B/A的一些相关操作,C/A的代码中涉及到和A/C的一些相关操作。B或C任意一个出了问题,对A都会有很大的影响。)
此时A和B,A和C都不再是直接交互了,而是通过队列来传递信息。(A只需要和队列交互,B、C也只需要和队列交互,B,C的存亡对A来说就没那么重要了(解耦合))
(1)上述描述的阻塞队列,并非是简单的数据结构。而是基于这个数据结构实现的服务器程序。
(2)整个系统的结构更复杂了,要维护的服务器就更多了
(3)效率在一定程度上变低了。请求从A发出来到B收到,这个过程中就经历队列的转发
这个过程中也是有一定开销的。
- 削峰填谷(此处所谓的峰谷都不是长时间持续的,而是短时间出现的)
一旦外界的请求出现了突发的峰值,就会影响到B、C服务器(严重情况下可能服务器直接挂了)。
A服务器的工作比较简单,每个请求消耗的资源相对较少。
B服务器(用户服务器)要从数据库中查找到对应的用户信息,消耗资源较多。请求过多的时候可能出问题。
C服务器(商品服务器)要从数据库中查找到对应的商品信息,有时还要对商品信息进行筛选。消耗资源较多,请求过多的时候可能出问题。
引入队列,就可以有效地防止B,C服务器因为请求过多而出现问题。
队列不实习业务逻辑,只是用来存储数据的,即使外界的请求出现峰值,也有一定的能力来承担。B,C按照原来的速度正常取数据即可。
2.3标准库中的阻塞队列
在 Java 标准库中内置了阻塞队列. 如果我们需要在一些程序中使用阻塞队列, 直接使用标准库中的即可。
- BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.
- put 方法用于阻塞式的入队列, take 用于阻塞式的出队列.
- BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
// 入队列
queue.put("abc");
// 出队列. 如果没有 put 直接 take, 就会阻塞.
String elem = queue.take();
2.4 阻塞队列的实现
- 先实现普通队列
先实现一个基于数组的环形队列
class MyBlockingQueue{
private String[] elems = null;
private int head = 0;
private int tail = 0;
private int size = 0;//队列当前元素数目
public MyBlockingQueue(int capacity){
elems = new String[capacity];
}
public void put(String element){
if(size > elems.length){//队列满了
//暂时不做处理,先完成一个普通队列
//后面再在这里做阻塞处理
return;
}
elems[tail++] = element;//新的元素放到tail指向的位置。
if(tail >= elems.length){
tail = 0;
}
size++;
}
public String take(){
if(size == 0){
//暂时不做处理,先完成一个普通队列
//后面再在这里做阻塞处理
return null; //队列为空
}
String element = elems[head++];//队头元素出队列
if(head >= elems.length){
head = 0;
}
size--;
return element;
}
}
public static void main(String[] args) {
MyBlockingQueue BQ = new MyBlockingQueue(2000);
BQ.put("1");
BQ.put("2");
BQ.put("3");
BQ.put("4");
BQ.put("5");
BQ.put("6");
String elem = " ";
elem = BQ.take();
System.out.println("elem :" + elem);
elem = BQ.take();
System.out.println("elem :" + elem);
elem = BQ.take();
System.out.println("elem :" + elem);elem = BQ.take();
System.out.println("elem :" + elem);
}
多线程情况下,假设elems的大小为10,此时此时thread1正在添加最后一个元素,同时thread2也在尝试添加元素,如果按下面的顺序就会出现线程安全问题:
thread1先执行,此时队列还没有满于是不进入if语句返回,继续向下执行。此时thread1被调度走,thread2开始执行,因为thread1还没有开始入队列的操作,所以,此时队列还没有满于是不进入if语句返回,继续向下执行。此时thread1继续执行,进行入队列的操作后,队列这时就是满的情况,但是thread2开始执行的时候会继续刚才的操作,入队列。这时之前入队列的最后一个元素就会被覆盖并且size也会超过队列长度。(take操作同理,也有上面问题)
这时就可以通过加锁来解决。
- 完善线程安全
要阻塞的地方和写操作的地方都要加锁。
class MyBlockingQueue{
private String[] elems = null;
private int head = 0;
private int tail = 0;
private int size = 0;//队列当前元素数目
public MyBlockingQueue(int capacity){
elems = new String[capacity];
}
private Object locker = new Object();//锁对象
public void put(String element){
synchronized(locker){
if(size > elems.length){//队列满了
//暂时不做处理,先完成一个普通队列
//后面再在这里做阻塞处理
return;
}
elems[tail++] = element;//新的元素放到tail指向的位置。
if(tail >= elems.length){
tail = 0;
}
size++;
}
}
public String take(){
String element = null;
synchronized (locker){
if(size == 0){
//暂时不做处理,先完成一个普通队列
//后面再在这里做阻塞处理
return null; //队列为空
}
element = elems[head++];//队头元素出队列
if(head >= elems.length){
head = 0;
}
size--;
}
return element;
}
}
队列满了后再入队列就需要阻塞。当元素出队列的时候唤醒。
队列空了后,再出队列就需要阻塞。当元素入队列的时候唤醒。
- 实现阻塞。
class MyBlockingQueue{
private String[] elems = null;
private int head = 0;
private int tail = 0;
private int size = 0;//队列当前元素数目
public MyBlockingQueue(int capacity){
elems = new String[capacity];
}
private Object locker = new Object();//锁对象
public void put(String element) throws InterruptedException {
synchronized(locker){
if(size > elems.length){//队列满了
//暂时不做处理,先完成一个普通队列
locker.wait();//后面再在这里做阻塞处理
}
elems[tail++] = element;//新的元素放到tail指向的位置。
if(tail >= elems.length){
tail = 0;
}
size++;
locker.notify();//入队列成功后唤醒。
}
}
public String take() throws InterruptedException {
String element = null;
synchronized (locker){
if(size == 0){
//暂时不做处理,先完成一个普通队列
locker.wait();//后面再在这里做阻塞处理
}
element = elems[head++];//队头元素出队列
if(head >= elems.length){
head = 0;
}
size--;
locker.notify();//出队列成功后唤醒。
}
return element;
}
}
但是上面的代码在多线程的情况下也会出现一种问题,入队列的唤醒操作可能会唤醒其他等待入队列的操作:
如果队列满了,此时又正好有同时有三个线程thread1,thread2,thread3。其中thread1和thread2都是执行put操作,都会进入wait阻塞状态(即使加锁了,第一个线程wait后就会释放锁,所以第二个线程也会进入wait阻塞状态)。此时thread3执行take操作,结束后将thread1从wait中唤醒,thread1执行入队列操作(队列再次满),执行到locker.notify();后又将thread2从wait中唤醒,可是此时队列已经满了,再入就会出现和之前一样的问题。
这种问题会出现的原因是用if来判断一次是否需要阻塞是远远不够的。在多线程的情况下,每次进入阻塞到被唤醒之间都会发生很多变数这个时候就难以保证此时还是不是满足wait的条件了。所以我们要在唤醒后再做一次判断,就能避免这个情况了。
这里可以将if语句变为while语句。这样就可以实现wait之前确定一次条件,wait被唤醒之后还能判断一次条件。(而且JAVA标准库也是推荐这么使用wait的)
阻塞队列:
class MyBlockingQueue{
private String[] elems = null;
private int head = 0;
private int tail = 0;
private int size = 0;//队列当前元素数目
public MyBlockingQueue(int capacity){
elems = new String[capacity];
}
private Object locker = new Object();//锁对象
public void put(String element) throws InterruptedException {
synchronized(locker){
while(size > elems.length){//队列满了
//暂时不做处理,先完成一个普通队列
locker.wait();//后面再在这里做阻塞处理
}
elems[tail++] = element;//新的元素放到tail指向的位置。
if(tail >= elems.length){
tail = 0;
}
size++;
locker.notify();//入队列成功后唤醒。
}
}
public String take() throws InterruptedException {
String element = null;
synchronized (locker){
while(size == 0){
//暂时不做处理,先完成一个普通队列
locker.wait();//后面再在这里做阻塞处理
}
element = elems[head++];//队头元素出队列
if(head >= elems.length){
head = 0;
}
size--;
locker.notify();//出队列成功后唤醒。
}
return element;
}
}
3. 定时器
定时器也是软件开发中的一个重要组件. 类似于一个 “闹钟”. 达到一个设定的时间之后, 就执行某个指定好的代码。
3.1 标准库中的定时器
- 标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule .
- schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后执行 (单位为毫秒)
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask(){
@Override
public void run() {//时间到了就会执行run里的代码
System.out.println("timer:3000");
}
}, 3000);
timer.schedule(new TimerTask(){
@Override
public void run() {
System.out.println("timer:2000");
}
}, 2000);
timer.schedule(new TimerTask(){
@Override
public void run() {
System.out.println("timer:1000");
}
}, 1000);
System.out.println("main");
System.out.println("timer 结束");
timer.cancel();//需要使用cancel主动结束。否则Timer不知道是否其他地方还要继续添加任务.
}
3.2 实现定时器
定时器就是可以在指定的时间,去执行某个任务(执行某一段代码)。要想实现一个定时器,就先要理解schedule的作用:把一个任务(TimerTask)添加到定时器中。定时器内部有一个线程,就会在任务时间到的时候,调用任务对应的代码。
其次还要明白定时器的构成
1)需要有一个线程,负责帮计时。等任务到达合适的时间,这个线程就负责执行。
2)需要一个带优先级的阻塞队列用来保存所有的任务。
- 使用MyTimerTask类(实现Comparable 接口)用于描述一个任务.
(1)需要一个 Runnable 对象(用来接受要执行的任务(run方法里的代码))
(2)需要一个time(用来表示执行任务的相对时间(毫秒时间戳))。
(3)需要一个getTime方法来获取执行任务的时间。
(4)需要一个构造方法来初始化Runnable和time(当前时间加上执行任务的相对时间得到的真正执行任务的绝对时间)
(5)需要一个run来执行任务
(6)重写compareTo方法(将时间短的任务放在前面)
有些集合类,是对于元素有特定要求的PriorityQueue,TreeMap,TreeSet.都要求元素是"可比较大小。Comparable,ComparatorHashMap,HashSet,则是要求元素是"可比较相等""可hash的"
2. 使用MyTimer类来表示一个定时器
(1)需要一个Thread对象用来负责扫描任务队列,执行任务。
(2)需要一个阻塞队列(其中的元素是MyTimerTask类)来作为优先级任务队列(优先级在MyTimerTask类中实现(实现 Comparable 接口))
(3)需要一个schedule方法,创建一个 MyTimeTask 对象接收任务内容和时间,并将这个 MyTimeTask 对象放进队列中。
(4)需要一个构造方法,创建扫描线程,让扫描线程来完成判定和执行。
class MyTimeTask implements Comparable<MyTimeTask> {
private long time;//执行任务的时间(ms级别的时间戳)
private Runnable runnable;//要执行的任务(run方法里的代码)
public long getTime() {
return time;
}
public MyTimeTask(Runnable runnable, long delay) {
this.runnable = runnable;
time = System.currentTimeMillis() + delay;//当前时间加上执行任务的相对时间就是真正执行任务的绝对时间。
}
public void run() { //执行任务
runnable.run();
}
@Override
public int compareTo(MyTimeTask o) {
return (int)(this.time - o.time);
}
}
class MyTimer {
private Thread thread = null;//负责扫描任务队列,执行任务的线程
private PriorityQueue<MyTimeTask> tasksQueue = new PriorityQueue<>();//任务队列
public void schedule(Runnable runnable, long delay) {
MyTimeTask task = new MyTimeTask(runnable, delay);
tasksQueue.offer(task);
}
public MyTimer() {//扫描线程就需要循环的反复的扫描队首元素,然后判定队首元素是不是时间到了,
//如果时间没到,啥都不干,如果时间到了,就执行这个任务并且把这个任务从队列中删除掉,
thread = new Thread(()->{
while (true) {
if (tasksQueue.isEmpty()) {//队列空了
//暂时不做处理
continue;
}
MyTimeTask task = tasksQueue.peek();//队列不为空,取出队首元素
long curTime = System.currentTimeMillis();
if (curTime >= task.getTime()) {//时间到了,可以完成任务了
tasksQueue.poll();
task.run();
}
else {
//时间还没到,暂时不处理。
continue;
}
}
});
thread.start();
}
}
但是上面的代码还要解决两个比较关键的问题
(1)线程安全
如果我们在主线程中对于队列中的元素进行添加,在其他线程中要在执行完任务后删除队列中的元素。两个不同的线程操作一个相同的任务队列 。多个线程操作同一个变量就会有线程安全问题。所以要加锁来解决。
其他地方一样
class MyTimer {
private Thread thread = null;//负责扫描任务队列,执行任务的线程
private PriorityQueue<MyTimeTask> tasksQueue = new PriorityQueue<>();//任务队列
private Object locker = new Object();//创建一个锁对象
public void schedule(Runnable runnable, long delay) {
synchronized(locker) {
MyTimeTask task = new MyTimeTask(runnable, delay);
tasksQueue.offer(task);
}
}
public MyTimer() {//扫描线程就需要循环的反复的扫描队首元素,然后判定队首元素是不是时间到了,
//如果时间没到,啥都不干,如果时间到了,就执行这个任务并且把这个任务从队列中删除掉,
thread = new Thread(()->{
while (true) {
synchronized(locker) {//在while里加锁避免循环不能结束导致锁无法被释放
if (tasksQueue.isEmpty()) {//队列空了
//暂时不做处理
continue;
}
MyTimeTask task = tasksQueue.peek();//队列不为空,取出队首元素
long curTime = System.currentTimeMillis();
if (curTime >= task.getTime()) {//时间到了,可以完成任务了
tasksQueue.poll();
task.run();
}
else {
//时间还没到,暂时不处理。
continue;
}
}//解锁
}
});
thread.start();
}
}
(2)线程饿死
while循环里,解锁之后,立即又重新尝试加锁了导致其他线程通过schedule想加锁,但是加不上(线程饿死).这时就可以用wait和notify
class MyTimer {
private Thread thread = null;//负责扫描任务队列,执行任务的线程
private PriorityQueue<MyTimeTask> tasksQueue = new PriorityQueue<>();//任务队列
private Object locker = new Object();//创建一个锁对象
public void schedule(Runnable runnable, long delay) {
synchronized(locker) {
MyTimeTask task = new MyTimeTask(runnable, delay);
tasksQueue.offer(task);
locker.notify();//添加新的元素之后,就可以唤醒扫描线程的wait了
}
}
public MyTimer() {//扫描线程就需要循环的反复的扫描队首元素,然后判定队首元素是不是时间到了,
//如果时间没到,啥都不干,如果时间到了,就执行这个任务并且把这个任务从队列中删除掉,
thread = new Thread(() -> {
while (true) {
try {
synchronized(locker) {//在while里加锁避免循环不能结束导致锁无法被释放
while (tasksQueue.isEmpty()) {//队列空了
locker.wait();//避免循环导致线程饿死
}
MyTimeTask task = tasksQueue.peek();//队列不为空,取出队首元素
long curTime = System.currentTimeMillis();
if (curTime >= task.getTime()) {//时间到了,可以完成任务了
tasksQueue.poll();
task.run();
}
else {
//当前时间还没到,暂时先不执行
//不能使用sleep,会错过新的任务,也无法释放锁。
locker.wait(task.getTime() - curTime);//解决“忙等”,时间没到却一直执行前面的判断
}
}//解锁
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
}
}
定时器:
class MyTimeTask implements Comparable<MyTimeTask> {
private long time;//执行任务的时间(ms级别的时间戳)
private Runnable runnable;//要执行的任务(run方法里的代码)
public long getTime() {
return time;
}
public MyTimeTask(Runnable runnable, long delay) {
this.runnable = runnable;
this.time = System.currentTimeMillis() + delay;//当前时间加上执行任务的相对时间就是真正执行任务的绝对时间。
}
public void run() { //执行任务
runnable.run();
}
@Override
public int compareTo(MyTimeTask o) {
return (int)(this.time - o.time);
}
}
class MyTimer {
private Thread thread = null;//负责扫描任务队列,执行任务的线程
private PriorityQueue<MyTimeTask> tasksQueue = new PriorityQueue<>();//任务队列
private Object locker = new Object();//创建一个锁对象
public void schedule(Runnable runnable, long delay) {
synchronized(locker) {
MyTimeTask task = new MyTimeTask(runnable, delay);
tasksQueue.offer(task);
locker.notify();//添加新的元素之后,就可以唤醒扫描线程的wait了
}
}
public MyTimer() {//扫描线程就需要循环的反复的扫描队首元素,然后判定队首元素是不是时间到了,
//如果时间没到,啥都不干,如果时间到了,就执行这个任务并且把这个任务从队列中删除掉,
thread = new Thread(() -> {
while (true) {
try {
synchronized(locker) {//在while里加锁避免循环不能结束导致锁无法被释放
while (tasksQueue.isEmpty()) {//队列空了
locker.wait();//避免循环导致线程饿死
}
MyTimeTask task = tasksQueue.peek();//队列不为空,取出队首元素
long curTime = System.currentTimeMillis();
if (curTime >= task.getTime()) {//时间到了,可以完成任务了
tasksQueue.poll();
task.run();
}
else {
//当前时间还没到,暂时先不执行
//不能使用sleep,会错过新的任务,也无法释放锁。
locker.wait(task.getTime() - curTime);//解决“忙等”,时间没到却一直执行前面的判断
}
}//解锁
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
}
}
4.线程池
线程池:把要使用的线程提前创建好。用完了也不要直接释放而是以备下次使用。就节省了创建/销毁线程的开销在这个使用的过程中,并没有真的频繁创建销毁,而只是从线程池里,取线程使用,用完了还给线程池.
这样虽然占用空间多了,但是减少了减少每次启动、销毁线程的损耗。
4.1 标准库中的线程池
使用 Executors.newFixedThreadPool(5) 能创建出固定包含 5 个线程的线程池.
返回值类型为 ExecutorService
通过 ExecutorService.submit 可以注册一个任务到线程池中.
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
Executors 创建线程池的几种方式 :
(1)newFixedThreadPool: 创建固定线程数的线程池
(2)newCachedThreadPool: 创建线程数目动态增长的线程池.
(3)newSingleThreadExecutor: 创建只包含单个线程的线程池.
(4)newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.
Executors 本质上是 ThreadPoolExecutor 类的封装.
ThreadPoolExecutor 提供了更多的可选参数, 可以进一步细化线程池行为的设定.
- int corePoolSize - 核心线程数:一个线程池里,最少得有多少个线程.
标准库提供的线程池,持有的线程个数并非是一成不变的,会根据当前任务量,自适应线程个数(任务非常多,就多搞几个线程;任务比较少,就少搞几个线程)
-
int maximumPoolSize - 最大线程数: 一个浅程地里最多能有多少个线程
-
long keepAliveTime - 保持存活时间:即非核心线程最大的可空闲时间(超过时间就销毁)
-
TimeUnit unit - 时间单位:s,min,ms,hour.…
-
BlockingQueue < Runnable > workQueue - 任务队列:存放线程池的多个任务的队列(可以设置PriorityBlockingQueue,且使用Runnable来作为描述任务的主体)
-
ThreadFactory threadFactory - 线程工厂 : 通过这个工厂类,来创建线程对象(Thread对象) (工厂模式,也是一种常见的设计模式。通过专门的"工厂类”/"工厂对象"来创建指定的对象)
-
RejectedExecutionHandler handler - 拒绝策略:上述参数中最重要的。
在线程池中,有一个阻塞队列。能够容纳的元素有上限的。当任务队列已经满了,如果继续往队列中添加任务,那么线程池会咋办。
(1)AbortPolicy:当任务队列已经满了,如果继续往队列中添加任务,新任务旧任务都不做了,直接抛出异常。
(2)CallerRunsPolicy:当任务队列已经满了,如果继续往队列中添加任务,新添加的任务由添加任务的线程来完成,任务队列还是执行原来的任务。
(3) DiscarduldestpoLlcy:当任务队列已经满了,如果继续往队列中添加任务,旧任务都不做了,直接开始完成新任务。
(4)DiscardPolicy: 当任务队列已经满了,如果继续往队列中添加任务,新任务都不做了,继续完成旧任务。
4.2 实现线程池
直接写一个固定线程数目的线程池(暂时不考虑线程的增加和减少)
(1)提供构造方法,指定创建多少个线程
(2)在构造方法中,把这些线程都创建好
(3)用一个阻塞队列,保存要执行的任务
(4)提供submit方法,可以添加新的任务。
class MyThreadPoolExecutor {
private List<Thread> threadList = new ArrayList<>();
private BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1000);
public MyThreadPoolExecutor(int n) {//创建n个线程
for (int i = 0; i < n; i++) {
Thread thread = new Thread(()->{//不断取出任务队列中的任务并执行
while (true) {
try {
Runnable runnable = queue.take();
runnable.run();
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
thread.start();
threadList.add(thread);
}
}
public void submit(Runnable runnable) throws InterruptedException {
queue.put(runnable);
}
}
public static void main(String[] args) throws InterruptedException {
MyThreadPoolExecutor executor = new MyThreadPoolExecutor(4);
for (int i = 0; i < 1000; i++) {
int n = i;
executor.submit(new Runnable(){
@Override
public void run() {
System.out.println("正在执行任务" + n + "当前线程为:" + Thread.currentThread().getName());
}
});
}
}
一定要注意,多个线程之间的执行顺序是不确定的,某个线程取到了某个任务了,但是并非立即就执行,这个过程中另一个线程就插到前面了此处的这些线程,彼此之间都是等价的.