JAVA并发编程(4)——(线程池,ThreadLocal)
JAVA并发编程(4)
线程池
11.1 概述
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结 束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线 程需要时间。
那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁, 而是可以继续执行其他的任务?
在 Java 中可以通过线程池来达到这样的效果。线程池里的每一个线程代码 结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来 使用。在 JDK5 之前,我们必须手动实现自己的线程池,从 JDK5 开始,Java 内置支持线程池.
在 JDK5 版本中增加了内置线程池实现 ThreadPoolExecutor,同时提供了 Executors 来创建不同类型的线程池。Executors 中提供了以下常见的线程池创 建方法:
newSingleThreadExecutor:一个单线程的线程池。如果因异常结束,会再创 建一个新的,保证按照提交顺序执行。
newFixedThreadPool:创建固定大小的线程池。根据提交的任务逐个增加线程, 直到最大值保持不变。如果因异常结束,会新创建一个线程补充。
newCachedThreadPool:创建一个可缓存的线程池。会根据任务自动新增或回 收线程。
newScheduledThreadPool:支持定时以及周期性执行任务的需求。
newWorkStealingPool:JDK8 新增,根据所需的并行层次来动态创建和关闭 线程,通过使用多个队列减少竞争,底层使用 ForkJoinPool 来实现。优势在于 可以充分利用多 CPU,把一个任务拆分成多个“小任务”,放到多个处理器核 心上并行执行;当多个“小任务”执行完成之后,再将这些执行结果合并起来即 可。
虽然在 JDK 中提供 Executors 类来支持以上类型的线程池创建,但通常情况下 不建议开发人员直接使用(见《阿里巴巴 java 开发规范》)。
11.2 ThreadPoolExecutor 类
java.uitl.concurrent.ThreadPoolExecutor 类是线程池中最核心的一个类, 因此如果要透彻地了解 Java 中的线程池,必须先了解这个类。
ThreadPoolExecutor 继承了 AbstractExecutorService 类,并提供了四个构造 器,事实上,通过观察每个构造器的源码具体实现,发现前面三个构造器都是调 用的第四个构造器进行的初始化工作。
构造器中各个参数的含义(7个)
corePoolSize:核心池的大小,这个参数跟后面讲述的线程池的实现原理有非 常大的关系。在创建了线程池后,默认情况下,在创建了线程池后,线程池中的 线程数为 0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线 程数目达到 corePoolSize 后,就会把到达的任务放到缓存队列当中;除非调用了 prestartAllCoreThreads()或者 prestartCoreThread()方法,从这 2 个方法的名 字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建 corePoolSize 个线程或者一个线程。
maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数, 它表示在线程池中最多能创建多少个线程;
keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情 况下,只有当线程池中的线程数大于 corePoolSize 时,keepAliveTime 才会起 作用,直到线程池中的线程数不大于 corePoolSize,即当线程池中的线程数大 于 corePoolSize 时,如果一个线程空闲的时间达到 keepAliveTime,则会终止, 直到线程池中的线程数不超过 corePoolSize。
unit:参数 keepAliveTime 的时间单位,有 7 种取值,在 TimeUnit 类中有 7 种静态属性:
workQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很 重要,会对线程池的运行过程产生重大影响
threadFactory:线程工厂,主要用来创建线程;
handler:表示当拒绝处理任务时的策略
七个参数设置实例:
11.3 线程池的执行
创建完成 ThreadPoolExecutor 之后,当向线程池提交任务时,通常使用 execute 方法。 execute 方法的执行流程图如下
文字描述线程池的执行:
-
如果线程池中存活的核心线程数小于线程数 corePoolSize 时,线程池会创 建一个核心线程去处理提交的任务。
-
如果线程池核心线程数已满,即线程数已经等于 corePoolSize,一个新提交 的任务,会被放进任务队列 workQueue 排队等待执
-
当线程池里面存活的线程数已经等于 corePoolSize 了,并且任务队列 workQueue 也满,判断线程数是否达到 maximumPoolSize,即最大线程 数是否已满,如果没到达,创建一个非核心线程执行提交的任务。
4.如果当前的线程数达到了 maximumPoolSize,还有新的任务过来的话,直 接采用拒绝策略处理。
11.4 线程池中的队列
线程池有以下工作队列:
ArrayBlockingQueue:有界队列,是一个用数组实现的有界阻塞队列,按 FIFO 排序量。
LinkedBlockingQueue:可设置容量队列,基于链表结构的阻塞队列,按 FIFO 排序任务,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列, 最大长度为 Integer.MAX_VALUE,吞吐量通常要高于 ArrayBlockingQuene;
11.5 线程池的拒绝策略
构造方法的中最后的参数 RejectedExecutionHandler 用于指定线程池的拒绝 策略。当请求任务不断的过来,而系统此时又处理不过来的时候,我们就需要采 取对应的策略是拒绝服务。
默认有四种类型:
AbortPolicy 策略:该策略会直接抛出异常,阻止系统正常工作。
CallerRunsPolicy 策略:只要线程池未关闭,该策略在调用者线程中运行当前的 任务(如果任务被拒绝了,则由**提交任务的线程(例如:main)**直接执行此任务)。
DiscardOleddestPolicy 策略:该策略将丢弃最老的一个请求,也就是即将被执 行的任务,并尝试再次提交当前任务。
DiscardPolicy 策略:该策略丢弃无法处理的任务,不予任何处理。
11.6 execute 与 submit 的区别
执行任务除了可以使用 execute 方法还可以使用 submit 方法。它们的主要区别 是:execute 适用于不需要关注返回值的场景,submit 方法适用于需要关注返 回值的场景。
11.7 关闭线程池
关闭线程池可以调用 shutdownNow 和 shutdown 两个方法来实现。
- shutdownNow:对正在执行的任务全部发出 interrupt(),停止执行,对还未开 始执行的任务全部取消,并且返回还没开始的任务列表。
- shutdown:当我们调用 shutdown 后,线程池将不再接受新的任务,但也不会 去强制终止已经提交或者正在执行中的任务。
11.8 创建线程的几种方式
创建线程有几种方法:4
继承Thread
实现Runnable
实现Callable
使用线程池
11.9 案例:
/*
执行的任务
*/
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();
}
}
结果:
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执行完毕
ThreadLocal
12.1 概述
在 java 的多线程模块中,ThreadLocal 是经常被提问到的一个知识点,提问的 方式有很多种,因此只有理解透彻了,不管怎么问,都能游刃有余。(基于 jdk1.8)
12.2 线程封闭
在了解 ThreadLocal 之前,我们先了解下什么是线程封闭.对象封闭在一个线程里,即使这个对象不是线程安全的,也不会出现并发安全问 题。
例如 栈封闭:就是用栈(stack)来保证线程安全
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DRo16MA5-1679209782940)(C:Users封纪元AppDataRoamingTypora ypora-user-images1642757779862.png)]
StringBuilder 是线程不安全的,但是它只是个局部变量,局部变量存储在虚拟机栈,虚拟机栈是线程隔离的,所以不会有线程安全问题.
ThreadLocal 线程封闭:简单易用
使用 ThreadLocal 来实现线程封闭,线程封闭的指导思想是封闭,而不是共享。
所以说 ThreadLocal 是用来解决变量共享的并发安全问题,多少有些不精确。
12.3 ThreadLocal 是什么
从名字我们就可以看到 ThreadLocal 叫做线程变量,意思是 ThreadLocal 中填 充的变量属于当前线程==,该变量对其他线程而言是隔离的==。ThreadLocal 为变量 在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
一个Threadlocal代码案例:
package threadlocal;
public class ThreadLocalDemo {
//创建一个ThreadLocal对象,用来为每个线程会复制保存一份变量,实现线程封闭
private static ThreadLocal<Integer> localNum = new ThreadLocal<Integer>(){
@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
}
}.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
}
}.start();
System.out.println(Thread.currentThread().getName()+":"+localNum.get());//0
}
}
结果:
12.4 ThreadLocal 原理分析
首先 **ThreadLocal 是一个泛型类,保证可以接受任何类型的对象。 因为一个线程内可以存在多个 ThreadLocal 对象**,**所以其实是 ThreadLocal 内部维护了一个 Map ,这个 Map 不是直接使用的 HashMap ,而是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类**。**而我们使用的 get()、set() 方法其实都是调用了这个 ThreadLocalMap 类对应的 get()、set() 方法。**
在ThreadLocal中使用set()和get()方法的实例:
set 方法
在set方法中用createMap方法创建ThreadLocalMap 的静态内部类
另外set在往ThreadLocalMap 存值的时候是以当前的ThreadLocal为键
最 终 的 变 量 是 放 在 了 当 前 线 程 的 ThreadLocalMap 中 , 并 不 是 存 在 ThreadLocal 上,ThreadLocal 可以理解为只是 ThreadLocalMap 的封装,传 递了变量值。
createMap 方法
get 方法
12.5 ThreadLocal 内存泄漏问题
TreadLocalMap 使用 ThreadLocal 的弱引用作为 key,如果一个 ThreadLocal 不存在外部强引用时,Key(ThreadLocal)势必会被 GC 回收,这样就会导致 ThreadLocalMap 中 key 为 null, 而 value 还存在着强引用,只有 thead 线程 退出以后,value 的强引用链条才会断掉
但如果当前线程再迟迟不结束的话,这些 key 为 null 的 Entry 的 value 就会一 直存在一条强引用链:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永远无法回收,造成内存泄漏
key 使用强引用
当 ThreadLocalMap 的 key 为 强 引 用 回 收 ThreadLocal 时 , 因 为 ThreadLocalMap 还 持 有 ThreadLocal 的 强 引 用 , 如 果 没 有 手 动 删 除 , ThreadLocal 不会被回收,导致 Entry 内存泄漏。
key 使用弱引用
当 ThreadLocalMap 的 key 为 弱 引 用 回 收 ThreadLocal 时 , 由 于 ThreadLocalMap 持 有 ThreadLocal 的 弱 引 用 , 即 使 没 有 手 动 删 除 , ThreadLocal 也会被回收。当 key 为 null,在下一次 ThreadLocalMap 调用 set(),get(),remove()方法的时候会被清除 value 值。
ThreadLocal 正确的使用方法
每次使用完 ThreadLocal 都调用它的 remove()方法清除数据.
相关文章
- Java实现基于朴素贝叶斯的情感词分析
- 图文详解Java对象内存布局
- JAVA使用几种对称加密算法
- JAVA使用几种非对称加密
- SimpleDateFormat 工具多线程环境下导致的严重问题
- Tomcat 下面使用软连接指向真实的上传文件夹
- Maven 编译项目缺失xml文件
- 各种排序算法实现(JAVA)
- 细谈Java中使用static变量,方法
- 细谈java中的数组
- 细谈java 的I/O
- 深入并发锁,解析Synchronized锁升级
- 含源码解析,深入Java 线程池原理
- 性能优于JDK代理,CGLib如何实现动态代理
- 阿里巴巴Java开发手册快速学习
- Java的RMI远程方法调用实现和应用
- HashMap在并发下可能出现的问题分析
- Java并发编程中的阻塞和中断
- 理解Java中的final和static关键字
- 理解Java中的引用传递和值传递