Java-并发编程-02线程池相关
在上一篇幅中对并发编程进行了简单介绍:并发与并行,进程与线程,以及并发编程的简单代码
但是在企业中往往并不能解决实际问题,例如:
1.synchronized关键字在企业开发中会大大降低系统的性能,有什么解决方式,或者其他的替代方案
2.当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。线程对象在不同的时期有不同的状态,如果不清楚认识,则无法清楚自己的代码出现的问题
3.随处可见的 new Thread(); 缺少对于线程、以及资源的管理,有什么解决方案?
查看资料:
https://javaguide.cn/java/concurrent/jmm.html#jmm-是如何抽象线程和主内存之间的关系
https://www.cnblogs.com/zhangxiann/p/13490598.html
https://www.iteye.com/topic/652440
https://www.cnblogs.com/dolphin0520/p/3920373.html
一、线程
1.线程状态
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。线程对象在不同的时期有不同的状态。那么Java中的线程存在哪几种状态呢?Java中的线程状态被定义在了java.lang.Thread.State枚举类中,State枚举类的源码如下:
点击查看代码
public enum State {
/**
* Thread state for a thread which has not yet started.
*/
/* 新建 */
NEW,
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
/* 可运行状态 */
RUNNABLE,
/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
*/
/* 阻塞状态 */
BLOCKED,
/**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
* <ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </ul>
*
* <p>A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called <tt>Object.wait()</tt>
* on an object is waiting for another thread to call
* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
* that object. A thread that has called <tt>Thread.join()</tt>
* is waiting for a specified thread to terminate.
*/
/* 无限等待状态 */
WAITING,
/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
/* 计时等待 */
TIMED_WAITING,
/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
/* 终止 */
TERMINATED;
}
线程状态 | 具体含义 |
---|---|
NEW | 一个尚未启动的线程的状态。也称之为初始状态、开始状态。线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread()只有线程象,没有线程特征。 |
RUNNABLE | 当我们调用线程对象的start方法,那么此时线程对象进入了RUNNABLE状态。那么此时才是真正的在JVM进程中创建了一个线程,线程一经启动并不是立即得到执行,线程的运行与否要听令与CPU的调度,那么我们把这个中间状态称之为可执行状态(RUNNABLE)也就是说它具备执行的资格,但是并没有真正的执行起来而是在等待CPU的调度。 |
BLOCKED | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。 |
WAITING | 一个正在等待的线程的状态。也称之为等待状态。造成线程等待的原因有两种,分别是调用Object.wait()、join()方法。处于等待状态的线程,正在等待其他线程去执行一个特定的操作。例如:因为wait()而等待的线程正在等待另一个线程去调用notify()或notifyAll();一个因为join()而等待的线程正在等待另一个线程结束。 |
TIMED_WAITING | 一个在限定时间内等待的线程的状态。也称之为限时等待状态。造成线程限时等待状态的原因有三种,分别是:Thread.sleep(long),Object.wait(long)、join(long)。 |
TERMINATED | 一个完全运行完成的线程的状态。也称之为终止状态、结束状态 |
2.线程状态转换
3.案例演示
案例一:
本案例主要演示TIME_WAITING的状态转换。
点击查看代码
package com.vayne.thread;
/**
* @author vayne
* @date 2023-10-31
*/
public class ThreadStatesDemo {
public static void main(String[] args) throws InterruptedException {
//定义一个内部线程
Thread thread = new Thread(() -> {
System.out.println("2.执行thread.start()之后,线程的状态:" + Thread.currentThread().getState());
try {
//休眠100毫秒
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("4.执行Thread.sleep(long)完成之后,线程的状态:" + Thread.currentThread().getState());
});
//获取start()之前的状态
System.out.println("1.通过new初始化一个线程,但是还没有start()之前,线程的状态:" + thread.getState());
//启动线程
thread.start();
//休眠50毫秒
Thread.sleep(50);
//因为thread1需要休眠100毫秒,所以在第50毫秒,thread处于sleep状态
//用main线程来获取thread1线程的状态,因为thread1线程睡眠时间较长
//所以当main线程执行的时候,thread1线程还没有睡醒,还处于计时等待状态
System.out.println("3.执行Thread.sleep(long)时,线程的状态:" + thread.getState());
//main线程主动休眠150毫秒,第150毫秒时,thread早已执行完毕
Thread.sleep(100);
System.out.println("5.线程执行完毕之后,线程的状态:" + thread.getState() + "\n");
}
}
案例二:
本案例主要演示WAITING的状态转换。
点击查看代码
package com.vayne.thread;
/**
* @author vayne
* @date 2023-10-31
*/
public class ThreadStatusWaitingDemo {
public static void main(String[] args) throws InterruptedException {
//定义一个对象,用来加锁和解锁
Object obj = new Object();
//定义一个内部线程
Thread thread1 = new Thread(() -> {
System.out.println("2.执行thread.start()之后,线程的状态:" + Thread.currentThread().getState());
synchronized (obj) {
try {
//thread1需要休眠100毫秒
Thread.sleep(100);
//thread1 100毫秒之后,通过wait()方法释放obj对象是锁
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("4.被object.notify()方法唤醒之后,线程的状态:" + Thread.currentThread().getState());
});
//获取start()之前的状态
System.out.println("1.通过new初始化一个线程,但是还没有start()之前,线程的状态:" + thread1.getState());
//启动线程
thread1.start();
//main线程休眠150毫秒
Thread.sleep(150);
//因为thread1在第100毫秒进入wait等待状态,所以第150秒肯定可以获取其状态
System.out.println("3.执行object.wait()时,线程的状态:" + thread1.getState());
//声明另一个线程进行解锁
new Thread(() -> {
synchronized (obj) {
//唤醒等待的线程
obj.notify();
}
}).start();
//main线程休眠10毫秒等待thread1线程能够苏醒
Thread.sleep(10);
//获取thread1运行结束之后的状态
System.out.println("5.线程执行完毕之后,线程的状态:" + thread1.getState() );
}
}
案例三:
本案例主要演示BLOCKED的状态转换。
点击查看代码
package com.vayne.thread;
import java.security.PublicKey;
/**
* @author vayne
* @date 2023-11-01
*/
public class ThreadStatusBlockedDemo {
//定义一个对象,用来加锁和解锁
public static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
////定义一个线程,先抢占了obj对象的锁
Thread thread2 = new Thread(() -> {
synchronized (obj) {
try {
Thread.sleep(1000);////第一个线程要持有锁1000毫秒
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
thread2.start();
//定义目标线程,获取等待获取obj的锁
Thread thread1 = new Thread(() -> {
System.out.println("2.执行thread.start()之后,线程的状态:" + Thread.currentThread().getState());
try {
Thread.sleep(100);//100毫秒后,thread1开始抢锁,但由于锁被thread2持有,抢不到,处于阻塞状态
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (obj) {
System.out.println("4.抢锁成功,线程的状态:" + Thread.currentThread().getState());
}
});
System.out.println("1.通过new初始化一个线程,但是还没有start()之前,线程的状态:" + thread1.getState());
thread1.start();
Thread.sleep(200);
System.out.println("3.抢锁失败后,线程的状态:" + thread1.getState());//此时线程正处于阻塞
Thread.sleep(1500);//1700毫秒后,线程一定执行完毕
System.out.println("5.线程执行完毕后,线程的状态:" + thread1.getState());
}
}
二、线程池
提到池,大家应该能想到的就是水池。水池就是一个容器,在该容器中存储了很多的水。那么什么是线程池呢?线程池也是可以看做成一个池子,在该池子中存储很多个线程。
线程池存在的意义:
系统创建一个线程的成本是比较高的,因为它涉及到与操作系统交互,当程序中需要创建大量生存期很短暂的线程时,频繁的创建和销毁线程对系统的资源消耗有可能大于业务处理是对系统资源的消耗,这样就有点"舍本逐末"了。针对这一种情况,为了提高性能,我们就可以采用线程池。线程池在启动的时,会创建大量空闲线程,当我们向线程池提交任务的时,线程池就会启动一个线程来执行该任务。等待任务执行完毕以后,线程并不会死亡,而是再次返回到线程池中称为空闲状态。等待下一次任务的执行。
1.自定义线程池的设计思路
与Java-并发编程-基础篇 中的生产者消费者案例模型相像
- 准备一个任务容器
- 一次性启动多个(2个)消费者线程
- 刚开始任务容器是空的,所以线程都在wait
- 直到一个外部线程向这个任务容器中扔了一个"任务",就会有一个消费者线程被唤醒
- 这个消费者线程取出"任务",并且执行这个任务,执行完毕后,继续等待下一次任务的到来
在整个过程中,都不需要创建新的线程,而是循环使用这些已经存在的线程
案例代码:
点击查看代码
package com.vayne.thread;
import java.util.concurrent.BlockingQueue;
/**
* @author vayne
* @date 2023-11-01
*/
public class ThreadPoolDemo {
/*线程池初始化线程的个数*/
private int poolSize;
/*任务容器*/
private BlockingQueue<Runnable> queue;
private ThreadPoolDemo() {
}
public ThreadPoolDemo(int poolSize, BlockingQueue<Runnable> queue, String ThreadName) {
this.poolSize = poolSize;
this.queue = queue;
for (int i = 0; i < poolSize; i++) {
new TaskThread(ThreadName + i).start();
}
}
public boolean submit(Runnable task) throws InterruptedException {
queue.put(task);
return true;
}
private class TaskThread extends Thread {
public TaskThread(String names) {
super(names);
}
@Override
public void run() {
while (true) {
try {
Runnable runnable = queue.take();
runnable.run();
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
package com.vayne.thread;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
/**
* @author vayne
* @date 2023-11-01
*/
public class ThreadPoolTest {
public static void main(String[] args) throws InterruptedException {
ThreadPoolDemo pool = new ThreadPoolDemo(2,
new ArrayBlockingQueue<>(5),
"线程池中的线程");
for (int i = 0; i < 10; i++) {
int finalI = i;
pool.submit(() -> {
System.out.println("当前线程是:" + Thread.currentThread().getName()+", 当前正在执行第" + finalI + "个任务");
System.out.println();
});
}
}
}
2.JDK中的线程池
Executors
JDK对线程池也进行了相关的实现,在真实企业开发中我们也很少去自定义线程池,而是使用JDK中自带的线程池。
我们可以使用Executors中所提供的静态方法来创建线程池。
获取线程池的方法:
通过不同的方法创建出来的多种线程池具有不同的特点:
Executors.newCachedThreadPool();
-->创建一个可缓存线程池,可灵活的去创建线程,并且灵活的回收线程,若无可回收,则新建线程。
Executors.newFixedThreadPool(int nThreads);
--> 初始化一个具有固定数量线程的线程池
Executors.newSingleThreadExecutor();
-->初始化一个具有一个线程的线程池,做完一个,再做一个,不停歇,直到做完,老黄牛性格
Executors.newSingleThreadScheduledExecutor();
-->初始化一个具有一个线程的线程池,支持定时及周期性任务执行,按照固定的计划去执行线程,一个做完之后按照计划再做另一个
这个方法返回的都是ExecutorService类型的对象(ScheduledExecutorService继承ExecutorService),而ExecutorService可以看做就是一个线程池,那么ExecutorService给我们提供了哪些方法供我们使用呢?
ExecutorService中的常见方法
Future<?> submit(Runnable task);
-->提交任务方法
void shutdown();
-->关闭线程池的方法
点击查看SingleThreadScheduledExecutor()代码
package com.vayne.thread;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* @author vayne
* @date 2023-11-01
*/
public class ThreadPoolExecutor {
public static void main(String[] args) throws InterruptedException {
/**
* 定时执行:executor.schedule
* command: 任务类对象
* delay : 延迟多长时间开始执行任务, 任务提交到线程池以后我们需要等待多长时间开始执行这个任务
* unit : 指定时间操作单元
*/
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
System.out.println("定时任务预启动时间" + new Date(System.currentTimeMillis()));
executor.schedule(() -> {
System.out.println("定时任务执行时间:" + new Date(System.currentTimeMillis()));
}, 3, TimeUnit.SECONDS);
/**
* 周期性执行:executor.scheduleAtFixedRate
* command: 任务类对象
* initialDelay: 延迟多长时间开始第一次该执行任务, 任务提交到线程池以后我们需要等待多长时间开始第一次执行这个任务
* period: 下一次执行该任务所对应的时间间隔
* unit: 指定时间操作单元
*/
System.out.println("周期任务预启动时间" + new Date(System.currentTimeMillis()));
executor.scheduleAtFixedRate(()->{
System.out.println("周期任务的当前执行时间:" + new Date(System.currentTimeMillis()) );
},5,3,TimeUnit.SECONDS);
Thread.sleep(20000);
executor.shutdown();
}
}
ThreadPoolExecutor
刚才我们是通过Executors中的静态方法去创建线程池的,通过查看源代码我们发现,其底层都是通过ThreadPoolExecutor构建的。比如:
点击查看代码
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
那么也可以使用ThreadPoolExecutor去创建线程池。
以下是完整的线程池构造方法:
点击查看线程池构造方法
public ThreadPoolExecutor(
//corePoolSize:核心线程的最大值,不能小于0
int corePoolSize,
//maximumPoolSize:最大线程数,不能小于等于0,maximumPoolSize >= corePoolSize
int maximumPoolSize,
//keepAliveTime:空闲线程最大存活时间,不能小于0
long keepAliveTime,
//unit:时间单位
TimeUnit unit,
//workQueue:任务队列,不能为null
BlockingQueue<Runnable> workQueue,
//threadFactory:创建线程工厂,不能为null
ThreadFactory threadFactory,
//handler:任务的拒绝策略,不能为null
RejectedExecutionHandler handler
}
参数详解:
参数一:corePoolSize:
核心线程数:是指线程池中长期存活的线程数。
这就好比古代大户人家,会长期雇佣一些“长工”来给他们干活,这些人一般比较稳定,无论这一年的活多活少,这些人都不会被辞退,都是长期生活在大户人家的。
参数二:maximumPoolSize
最大线程数:线程池允许创建的最大线程数量,当线程池的任务队列满了之后,可以创建的最大线程数。
这是古代大户人家最多可以雇佣的人数,比如某个节日或大户人家有人过寿时,因为活太多,仅靠“长工”是完不成任务,这时就会再招聘一些“短工”一起来干活,这个最大线程数就是“长工”+“短工”的总人数,也就是招聘的人数不能超过 maximumPoolSize。
注意:最大线程数 maximumPoolSize 的值不能小于核心线程数 corePoolSize,否则在程序运行时会报 IllegalArgumentException 非法参数异常
参数三:keepAliveTime
空闲线程存活时间,当线程池中没有任务时,会销毁一些线程,销毁的线程数=maximumPoolSize(最大线程数)-corePoolSize(核心线程数)。
还是以大户人家为例,当大户人家比较忙的时候就会雇佣一些“短工”来干活,但等干完活之后,不忙了,就会将这些“短工”辞退掉,而 keepAliveTime 就是用来描述没活之后,短工可以在大户人家待的(最长)时间。
参数四:TimeUnit
时间单位:空闲线程存活时间的描述单位,此参数是配合参数 3 使用的。
参数五BlockingQueue
阻塞队列:线程池存放任务的队列,用来存储线程池的所有待执行任务。具体的阻塞队列可以参考Java-并发编程-基础篇 比较常用的是 LinkedBlockingQueue,线程池的排队策略和 BlockingQueue 息息相关。
参数六:ThreadFactory
线程工厂:线程池创建线程时调用的工厂方法,通过此方法可以设置线程的优先级、线程命名规则以及线程类型(用户线程还是守护线程)等。
点击查看默认的线程工厂源代码
static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
参数7:RejectedExecutionHandler
拒绝策略:当线程池的任务超出线程池队列可以存储的最大值之后,执行的策略。
默认的拒绝策略有以下 4 种:
AbortPolicy:拒绝并抛出异常。
CallerRunsPolicy:使用当前调用的线程来执行此任务。
DiscardOldestPolicy:抛弃队列头部(最旧)的一个任务,并执行当前任务。
DiscardPolicy:忽略并抛弃当前任务。
线程池的默认策略是 AbortPolicy 拒绝并抛出异常。
线程池的执行过程
请看下方图示:
当我们通过submit方法向线程池中提交任务的时候,具体的工作流程如下:
- 客户端每次提交一个任务,线程池就会在核心线程池中创建一个工作线程来执行这个任务。当核心线程池中的线程已满时,则进入下一步操作。
- 把任务试图存储到工作队列中。如果工作队列没有满,则将新提交的任务存储在这个工作队列里,等待核心线程池中的空闲线程执行。如果工作队列满了,则进入下个流程。
- 线程池会再次在非核心线程池区域去创建新工作线程来执行任务,直到当前线程池总线程数量超过最大线程数时,就是按照指定的任务处理策略处理多余的任务。
为什么线程池在执行过程中,当核心线程数corePoolSize达到最大,不直接新建临时线程呢?
这是我们每向池中丢一个任务,执行对应execute的源码
我们看到核心方法其实是addWorker,而它如何保证了原子性呢?
看到底层利用原子类加自旋锁保证了原子性,其实对于线程池类ThreadPoolExecutor本身也有private final ReentrantLock mainLock = new ReentrantLock();
可以看到在核心线程数满的情况下,先比于新增线程,在资源耗费上远远不如直接放在工作队列中来的简单。