目录
2.2.2 添加守护线程后--t.setDaemon(true)
4.1 ThreadPoolExecutor() 七参数详解
5.1 线程池为什么最好使用ThreadPoolExecutor的方式创建?
1.线程与进程
1.1 进程与线程的概念
进程是程序的一次动态执行过程,通常讲计算机正在执行的程序就是进程,每个程序都会对应着一个进程。(进程包括从代码加载到执行完成的一个完整过程,是操作系统资源分配的最小单元)
线程是进程中执行运算的最小单位,是被系统独立调度和分配的基本单位。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。线程可以对进程所有的资源进行调度和运算。线程既可以由操作系统内核来控制调度,也可以由用户程序进行控制调度。
每个进程至少有一个线程,反过来一个线程只能属于一个进程,可与同属一个进程的其它线程共享进程所拥有的全部资源。
1.2 区别
- 切换:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。
- 调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位
- 并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行
- 拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源.
- 系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。
2.守护线程
2.1 概念
守护线程,也称后台线程,是指在程序运行的时候提供一种通用服务的线程,它独立于控制终端并且周期性地执行某种任务或等待处理某些发生地食物。(即守护在其他线程全部运行结束后结束)
例如:GC(垃圾回收机制),后台音乐播放器。
2.2 实例
2.2.1 未添加守护线程时
运行结果:线程交替进行,拥有cpu时间片资源就执行。当主线程10次循环执行完成后,用户线程会一直执行。
public class DaemonDemo1 extends Thread{
//用户线程
@Override
public void run() {
while(true) {
System.out.println("线程运行中......");
try {
sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new DaemonDemo1();
t1.start();
//主线程
for (int i = 0; i < 10; i++) {
System.out.println("main");
Thread.sleep(200);
}
}
}
执行结果如下图所示:
2.2.2 添加守护线程后--t.setDaemon(true)
运行结果:将用户线程变成守护线程,当最后一个线程执行完毕,守护线程也随之结束。
public class DaemonDemo1 extends Thread{
//用户线程
@Override
public void run() {
while(true) {
System.out.println("线程运行中......");
try {
sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new DaemonDemo1();
//添加守护线程-->将用户线程变成守护线程
t1.setDaemon(true);
t1.start();
//主线程
for (int i = 0; i < 10; i++) {
System.out.println("main");
Thread.sleep(200);
}
}
}
3.ThreadLocal--线程本地化
ThreadLocal将内存中的数据复制到各个进程中,分别在进程中进行操作,互不干扰--以保证线程安全。(以空间换时间)
- ThreadLocal():创建一个线程本地变量
- get():返回此线程局部变量的当前线程副本的值
- initialValue():返回此线程局部变量的当前线程的"初始值"
- set(T value):将此线程局部变量的当前线程副本中的值设为value
public class Demo {
//匿名内部类
public static final ThreadLocal<Integer> local = new ThreadLocal<Integer>(){
//重写初始值方法--在ThreadLocl源码中
protected Integer initialValue() {
//定义初始值为0
return 0;
}
};
public static class DemoThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 5; i++) {
int cur = local.get();
local.set(cur+1);
System.out.println(this.getName()+":"+local.get());
}
}
}
public static void main(String[] args) {
Thread t1 = new DemoThread();
t1.setName("A");
Thread t2 = new DemoThread();
t2.setName("B");
Thread t3 = new DemoThread();
t3.setName("C");
t1.start();
t2.start();
t3.start();
}
}
执行结果如下图所示:
4.线程池的构造方法
4.1 ThreadPoolExecutor() 七参数详解
//创建一个新 ThreadPoolExecutor给定的初始参数。
public ThreadPoolExecutor( int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize:线程池中的核心线程数,即线程池中保留的线程个数。(注:即使有些线程处于空闲,也不会被销毁,除非采用ThreadPoolExecutor的allowCoreThreadTimeOut(true)方法开启了核心线程的超时策略)
- maximumPoolSize:线程池中允许存在线程的最大值。
- keepAliveTime:设置核心线程数量以外的线程的最大等待时间,超出时间没有新任务的线程会被销毁。
- unit:超出时间的单位。
- workQueue:线程队列。(保存通过Execute方法提交的,等待被执行的任务)
- threadFactory:线程创建工程。(即如何创建线程)
- handler:拒绝策略。(当提交的线程数量超出maximumPoolSize后,使用什么策略处理超出线程)
注:使用该构造方法创建线程池,满足以下参数条件,否则将抛出IIIegalArgumentException异常.
- corePoolSize不能小于0
- keepAlivetime不能小于0
- maximumPoolSize不能小于等于0
- maximumPoolSize不能小于corePoolSize
另外,workQueue、threadFactory和handler不能为null,否则抛出空指针异常。
4.2 执行流程
当调用 execute() 方法添加一个任务时,线程池会做如下判断:
- 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
- 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
- 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
- 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常RejectExecutionException。
当一个线程完成任务时,它会从队列中取下一个任务来执行。
当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到corePoolSize 的大小 。
4.3 实例
public class ThreadPoolDemo{
public static void main(String[] args) {
//初始化对象
ThreadPoolExecutor tpe = new ThreadPoolExecutor(1, 2, 10, TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(1),
(ThreadFactory) Thread::new,
new ThreadPoolExecutor.AbortPolicy());
System.out.println("线程池创建完成!");
//execute()--添加任务
tpe.execute(() -> sleep(100));
tpe.execute(() -> sleep(100));
tpe.execute(() -> sleep(100));
tpe.execute(() -> sleep(100));
int activeCount = -1;
int queueSize = -1;
while(true) {
//判断当前线程池中活跃线程或工作队列是否存在值,存在-则输入一下信息
if (activeCount != tpe.getActiveCount() || queueSize!=tpe.getQueue().size()) {
System.out.println("活跃线程个数:"+tpe.getActiveCount());
System.out.println("核心线程数:"+tpe.getCorePoolSize());
System.out.println("队列线程数:"+tpe.getQueue().size());
System.out.println("最大线程数:"+tpe.getMaximumPoolSize());
System.out.println("----------------------------------");
activeCount = tpe.getActiveCount();
queueSize = tpe.getQueue().size();
}
}
}
public static void sleep(long value) {
try {
System.out.println(Thread.currentThread().getName()+"线程执行sleep方法");
TimeUnit.SECONDS.sleep(value);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果如下所示:(出现报红--因为拒绝策略)
4.4 拒绝策略
策略 | 解释 |
ThreadPoolExecutor.AbortPolicy() | 默认,队列满了丢任务抛出异常 |
ThreadPoolExecutor.BiscardPolicy() | 队列满了丢任务不抛异常 |
ThreadPoolExecutor.DiscardOldestPolicy() | 将最早进入队列的任务删除,之后再尝试加入队列 |
ThreadPoolExecutor.CallerRunsPolicy() | 若添加到线程池失败,那么主线程会自己去执行该任务 |
5.线程池的创建规则
5.1 线程池为什么最好使用ThreadPoolExecutor的方式创建?
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor方式,这样的处理方式使得更清楚线程池的运行规则,避免资源耗尽的风险。
Executors返回的线程池对象的弊端:
FixedThreadPool和SingleThreadPool:
允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM(内存耗尽)。
CachedThreadPool:
允许的创建线程的数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
5.2 创建规则
这几个线程池理论是都可以接收无限个任务,所以这就有内存溢出的风险。实际上只要我们掌握了ThreadPoolExecutor构造函数7个参数的含义,我们就可以根据不同的业务来创建出符合需求的线程池。一般线程池的创建可以参考如下规则:
- IO密集型任务:IO密集型任务线程并不是一直在执行任务,应该配置尽可能多的线程,线程池线程数量推荐设置为2 * CPU核心数;对于IO密集型任务,网络上也有另一种线程池数量计算公式:CPU核心数/(1 - 阻塞系数),阻塞系数取值0.8~0.9,至于这两种公式使用哪一个,可以根据实际环境测试比较得出;
- 计算密集型任务:此类型需要CPU的大量运算,所以尽可能的去压榨CPU资源,线程池线程数量推荐设置为CPU核心数 + 1。
6.线程池的关闭
当线程池中所有任务都处理完毕后,线程并不会自己关闭。我们可以通过调用 shutdown 和
shutdownNow 方法来关闭线程池。两者的区别在于:
- shutdown 方法将线程池置为shutdown状态,拒绝新的任务提交,但线程池并不会马上关闭,而是等待所有正在执行的和线程队列里的任务都执行完毕后,线程池才会被关闭。所以这个方法是平滑的关闭线程池。ThreadPoolExecutor.shutdown();
- shutdownNow 方法将线程池置为stop状态,拒绝新的任务提交,中断正在执行的那些任务,并且清除线程队列里的任务并返回。所以这个方法是比较“暴力”的。List<Runnable> runnables = ThreadPoolExecutor.shutdownnow();//马上返回,并返回未执行的任务