重点知识学习(8.4)--[线程池 , ThreadLocal]
Posted 小智RE0
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了重点知识学习(8.4)--[线程池 , ThreadLocal]相关的知识,希望对你有一定的参考价值。
文章目录
1.线程池
说到线程的连接释放, 与数据库进行连接时,就是需要先创建连接,使用完后,进行销毁;这样频繁地创建连接以及销毁连接是比较耗费时间的; 后来学了数据库连接池, 比如说德鲁伊数据库连接池,进行相关配置后,指定数据库连接池的链接数量,最小连接数,最大连接数等参数; 数据库连接时从连接池取到即可,用完后不用销毁,放回连接池即可;
线程池也就是这样;在并发量过大的情况下,使用线程池无疑是一种最好的选择;
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率
可通过线程池来达到这样的效果。线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用。
从 JDK5 开始,Java语言中内置支持线程池
提供了Executors 来创建不同类型的线程池。实际开发不建议使用
Executors 中提供了以下常见的线程池创建方法:
- newSingleThreadExecutor:一个单线程的线程池。如果因异常结束,会再创建一个新的,保证按照提交顺序执行。
- newFixedThreadPool:创建固定大小的线程池。根据提交的任务逐个增加线程,直到最大值保持不变。如果因异常结束,会新创建一个线程补充。
- newCachedThreadPool:创建一个可缓存的线程池。会根据任务自动新增或回
收线程。- newScheduledThreadPool:支持定时以及周期性执行任务的需求。
- newWorkStealingPool:JDK8 新增,根据所需的并行层次来动态创建和关闭线程,通过使用多个队列减少竞争,底层使用 ForkJoinPool 来实现。优势在于可以充分利用多 CPU,把一个任务拆分成多个“小任务”,放到多个处理器核心上并行执行;当多个“小任务”执行完成之后,再将这些执行结果合并起来即可。
java.uitl.concurrent.ThreadPoolExecutor 类
是线程池的核心类
线程池构造函数中的7个参数
corePoolSize:表示创建的核心线程池数量
, 其实在创建线程池后核心线程池数量默认为0
, 发现要执行的任务后,才会去创建线程去执行,或者调用prestartAllCoreThreads()或者 prestartCoreThread()方法
,进行预创建.
核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,在创建了线程池后,线程池中的线程数为 0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到 corePoolSize 后,就会把到达的任务放到缓存队列当中;除非调用了prestartAllCoreThreads()或者 prestartCoreThread()方法,从这 2 个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize 个线程或者一个线程。
maximumPoolSize:表示在线程池中最多能创建多少个线程
keepAliveTime:非核心线程池中的线程,在没有执行任务时的存活时间
.
参数设置线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于 corePoolSize 时,keepAliveTime 才会起作用,直到线程池中的线程数不大于 corePoolSize,即当线程池中的线程数大于 corePoolSize 时,如果一个线程空闲的时间达到 keepAliveTime,则会终止,
直到线程池中的线程数不超过 corePoolSize。
unit:参数 keepAliveTime 的时间单位
,
在 TimeUnit 类中有这几种静态属性:
workQueue:作为阻塞队列,用来存储等待执行的任务
threadFactory:线程工厂,用来创建线程;
handler:设置拒绝处理任务时的策略.
线程池的执行过程
创建 ThreadPoolExecutor 线程池后,当向线程池提交任务时,常用的是 execute 方法。
execute 方法的执行图:
-
如果线程池中在
corePoolSize
中存活的核心线程数小于线程数时,线程池会创建一个核心线程去执行处理提交的任务。 -
但是如果线程池核心线程数
corePoolSize
已满了,那么来了一个新提交的任务,就会被放进任务队列workQueue
中排队等待执行。 -
当线程池里面存活的线程数已经等于 corePoolSize 了,且
任务队列workQueue 也满
,这时去判断线程数是否达到最大线程容纳数maximumPoolSize
,如果没到达,可以考虑创建一个非核心线程执行提交的任务。 -
如果当前的线程总数达到了最大线程容纳数
maximumPoolSize
,要是还来新的任务,就得采用拒绝策略处理。
线程的执行工作队列
-
ArrayBlockingQueue
:数组实现的有界阻塞队列
,按 FIFO的排序量。 -
LinkedBlockingQueue
:基于链表结构的阻塞队列
,按 FIFO排序任务,
它的容量可以进行设置,不设置的话,默认是无边界的阻塞队列
,
最大长度为Integer.MAX_VALUE
,吞吐量通常要高于ArrayBlockingQuene
;
execute 与 submit 的区别
其实区别就是submit
执行时具有返回值
execute 适用于不需要返回值的场景,submit 方法适用于需要返回值
的场景。
可调用get方法获取返回值结果
四个类型拒绝策略
参数RejectedExecutionHandler
用于指定线程池的拒绝策略。
-
AbortPolicy 策略:直接抛出异常,阻止系统正常工作。
-
DiscardOleddestPolicy 策略:丢弃最老的一个请求(即将被执行的任务),并尝试再次提交当前任务。
-
DiscardPolicy 策略:丢弃无法处理的任务,不予任何处理.
-
CallerRunsPolicy 策略:只要线程池还没有关闭,就会在调用者线程中运行当前的任务(
如果任务被拒绝了,则由提交任务的线程(例如:main)直接执行此任务
)。
关闭线程池方法 shutdownNow 和 shutdown
- shutdownNow() 方法 :立即关闭线程池,正在执行的任务执行
interrupt()方法
,停止执行,还没开始执行的任务被全部取消,返回还没开始的任务列表。 - shutdown( ) 方法 : 正常关闭线程池,注意它会等待已经提交或执行的任务执行完成; 且线程池不再接受新的任务
案例
/*
执行的任务
*/
public class MyTask implements Runnable
private int taskNum;
public MyTask(int num)
this.taskNum = num;
@Override
public void run()
try
Thread.currentThread().sleep(6000);
catch (InterruptedException e)
e.printStackTrace();
System.out.println(Thread.currentThread().getName()+":任务 "+taskNum+"执行完毕");
测试执行
public class Test
public static void main(String[] args)
//创建线程池, 核心线程池容量为 3;最大线程数为 8;
ThreadPoolExecutor executor = new ThreadPoolExecutor(
3, 8, 200,
TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(2),
Executors.defaultThreadFactory(),
//采用丢弃最老请求策略;
new ThreadPoolExecutor.DiscardOldestPolicy());
for (int i = 1; i <= 10; i++)
MyTask myTask = new MyTask(i);
executor.execute(myTask);//添加任务到线程池
executor.shutdown();
执行结果之1
pool-1-thread-1:任务 1执行完毕
pool-1-thread-2:任务 2执行完毕
pool-1-thread-3:任务 3执行完毕
pool-1-thread-4:任务 6执行完毕
pool-1-thread-5:任务 7执行完毕
pool-1-thread-6:任务 8执行完毕
pool-1-thread-7:任务 9执行完毕
pool-1-thread-8:任务 10执行完毕
pool-1-thread-2:任务 5执行完毕
pool-1-thread-1:任务 4执行完毕
2.ThreadLocal(线程变量)
关于线程封闭
对象封闭在一个线程里,即使这个对象不是线程安全的,也不会出现并发安全问题。
例如 栈封闭:就是用栈(stack)来保证线程安全
public void testThread()
StringBuilder s = new StringBuilder();
s.append("Hello");
StringBuilder 是线程不安全的,但是它只是个局部变量,局部变量存储在虚拟机栈,虚拟机栈是线程隔离的,所以不会有线程安全问题
- 线程封闭的指导思想是封闭,而不是共享。
- ThreadLocal 是用来解决变量共享的并发安全问题
初探执行原理
ThreadLocal 为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
达到线程隔离的效果;互相执行各自的操作,互不影响;
其中维护了一个ThreadLocalMap
集合;
ThreadLocalMap
作为ThreadLocal
的内部类
其中使用了set() 方法,get() 方法其实就是调用了ThreadLocalMap
中的方法;
比如说这个set方法;
-
调用set方法时, 先获取正在执行的线程对象, 为当前线程创建
ThreadLocalMap对象
-
ThreadLocalMap的键
就是ThreadLocal对象 , 值就是输入的值;
若当前线程中,没有Map;就去创建map,注意这时的key其实还是当前的ThreadLocal对象
然后看看get方法;
先根据自己线程去找ThreadLocal对象
维护的的ThreadLocalMap
集合
ThreadLocal带来的内存泄漏问题
“存储渗漏”。严格来说,只有对象不会再被程序用到了,但是 GC 又不能回收他们的情况,才叫内存泄漏。
尽管内存泄漏并不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被逐步蚕食,直至耗尽所有内存,最终出现 OutofMemory 异常,导致程序崩溃。
注意,这里的存储空间并不是指物理内存,而是指虚拟内存大小,这个虚拟内存大小取决于磁盘交换区设定的大小。
TreadLocalMap
中 使用了ThreadLocal
的弱引用作为 key,如果一个ThreadLocal
不存在外部强引用时,Key(ThreadLocal)势必会被 GC 回收,
这样就会导致ThreadLocalMap
中 key 为 null, 而 value 还存在着强引用,当 thead 线程退出以后,value 的强引用链条才会断掉
。
注意:要是线程还不结束;那么这些key 为 null 的 Entry 的强引用 value
就会一直存在一条强引用链:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
那么就会产生内存泄漏
现在分析一下TreadLocalMap
中 的Key使用强引用 / 弱引用的情况;
-
当 ThreadLocalMap 的 key 为 强 引 用的ThreadLocal 时
, 由于ThreadLocalMap 还 持 有 ThreadLocal 的 强 引 用 , 若不使用 手 动 删 除 ,ThreadLocal 不会被回收,还是会导致 Entry 内存泄漏。 -
当 ThreadLocalMap 的 key 为 弱 引 用 的ThreadLocal 时
, 由 于ThreadLocalMap 持 有 ThreadLocal 的 弱 引 用 ,即 使 没 有 手 动 删 除 ,ThreadLocal 也会被回收
。当 key 为 null 后,在下一次ThreadLocalMap 调用set(),get(),remove()方法的时候
就会清除 value 值。
建议 每次使用完 ThreadLocal 都调用它的 remove()方法清除数据 ,防止出现内存泄漏
案例
public class ThreadLocalDemo
//创建一个ThreadLocal对象,用来为每个线程会复制保存一份变量,实现线程封闭
private static ThreadLocal<Integer> localNum = new ThreadLocal<Integer>()
//初始化 ThreadLocal中的默认值
@Override
protected Integer initialValue()
return 0;
;
public static void main(String[] args)
new Thread()
@Override
public void run()
//设置副本的数值;
localNum.set(1);
try
Thread.sleep(2000);
catch (InterruptedException e)
e.printStackTrace();
localNum.set(localNum.get() + 10);
System.out.println(Thread.currentThread().getName() + ":" + localNum.get());//11
//当线程中不再使用线程变量时,将变量值清除
localNum.remove();
.start();
new Thread()
@Override
public void run()
//设置副本的数值;
localNum.set(3);
try
Thread.sleep(2000);
catch (InterruptedException e)
e.printStackTrace();
localNum.set(localNum.get() + 20);
System.out.println(Thread.currentThread().getName() + ":" + localNum.get());//23
//当线程中不再使用线程变量时,将变量值清除
localNum.remove();
.start();
//主线程的操作数,用的是默认的0;
System.out.println(Thread.currentThread().getName() + ":" + localNum.get());//0
以上是关于重点知识学习(8.4)--[线程池 , ThreadLocal]的主要内容,如果未能解决你的问题,请参考以下文章