多线程(六):线程池
线程的缺点:
- 线程的创建需要开辟内存资源:本地方法栈、虚拟机栈、程序计数器等线程私有变量的内存。频繁地创建和销毁线程,会带来一定的性能开销。
- 使用线程不能很好的管理任务和友好的拒绝任务。
在《Java开发手册》中,也有提到。
线程池的定义
使用池化技术来管理和使用线程的技术,就叫做线程池。
线程池创建线程是懒加载的
在有任务的时候才会创建线程,并不是创建线程池的时候就创建了线程。
线程池优点
线程池优点
- 避免频繁创建和销毁线程所带来的性能开销。
- 可以优化地拒绝任务。
- 更多功能,例如:执行定时任务。
线程池的创建方式(7种)
线程池的创建方式①:创建固定个数的线程池
public class ThreadPoolDemo45 {
public static void main(String[] args) {
//创建固定个数线程的线程池,参数为线程个数。
ExecutorService service = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++) {
//执行任务
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程名:" + Thread.currentThread().getName());
}
});
}
}
}
该代码的执行结果如下:
注意:创建5个线程来执行2个任务如下面这段代码代码,问当前程序创建了几个线程?
public class ThreadPoolDemo45 {
public static void main(String[] args) {
//创建固定个数线程的线程池,参数为线程个数。
ExecutorService service = Executors.newFixedThreadPool(5);
for (int i = 0; i < 2; i++) {
//执行任务
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程名:" + Thread.currentThread().getName());
}
});
}
}
}
使用 jconsole 工具可见,当前程序只创建了两个线程。
有这个题目进而引出线程池的执行流程:
当拿到一个任务之后,会判断当前线程池里面的线程数量是否达到了最大值,如果没有达到,就会创建新的线程执行任务;当获得任务之后,线程池的数量已经是最大值,并且没有空闲的线程,当前任务会被放到线程池的任务队列里面等待执行。
自定义线程池行为
例如,设置线程池的命名规则和优先级,代码如下:
public class ThreadPoolDemo46 {
private static int count = 1;
static class MyThreadFactory implements ThreadFactory {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
//设置线程池命名
thread.setName("myThreadPool-" + count++);
//设置线程池优先级
thread.setPriority(10);
return thread;
}
}
public static void main(String[] args) {
//线程工厂
MyThreadFactory myThreadFactory = new MyThreadFactory();
//创建线程池
ExecutorService service =
Executors.newFixedThreadPool(10, myThreadFactory);
for (int i = 0; i < 10; i++) {
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程名:" + Thread.currentThread().getName()
+ ",线程优先级:" + Thread.currentThread().getPriority());
}
});
}
}
}
该代码的执行结果为:
缺点:任务数趋向无限大,从而线程数不可控。
线程池的创建方式②:创建带缓存的线程池
根据任务的数量生成对应的线程数,所以适用于:有大量的短期任务。
创建线程池代码如下:
public class ThreadPoolDemo47 {
public static void main(String[] args) {
//创建带缓存的线程池
ExecutorService service = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程名:" + Thread.currentThread().getName());
}
});
}
}
}
该代码的执行结果如下:
线程池的创建方式③:创建可以执行定时任务的线程池
使用场景:需要定时执行任务,线程池的创建方法如下:
使用service.scheduleWithFixedDelay
执行任务:
public class ThreadPoolDemo48 {
public static void main(String[] args) {
//创建一个执行定时任务的线程池
ScheduledExecutorService service =
Executors.newScheduledThreadPool(10);
System.out.println("执行任务之前:" + new Date());
//执行任务
service.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
System.out.println("执行任务:" + new Date());
}
}, 1, 3, TimeUnit.SECONDS);//延迟1秒,每3秒执行一次
}
}
其中service.scheduleWithFixedDelay(参数1,参数2,参数3,参数4)
参数1 为线程池需要执行的任务;参数2 为定时任务的延迟时间;参数3 为定时任务的执行频率;参数4 为配合参数2和参数3使用的时间单位。
该代码的执行效果如下:
使用service.scheduleAtFixedRate
执行任务:
public class ThreadPoolDemo50 {
public static void main(String[] args) {
//创建一个执行定时任务的线程池
ScheduledExecutorService service =
Executors.newScheduledThreadPool(10);
System.out.println("执行任务之前:" + new Date());
//执行任务
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("执行任务:" + new Date());
}
}, 1, 3, TimeUnit.SECONDS);//延迟1秒,每3秒执行一次
}
}
该代码的执行结果如下:
我们发现使用service.scheduleWithFixedDelay
和使用service.scheduleAtFixedRate
执行简单任务时,代码的执行效果是一样的。
使用service.scheduleWithFixedDelay
执行任务,下一次任务的开始执行时间是以上一次任务执行的结束时间作为开始执行时间的。
而使用sservice.scheduleAtFixedRate
执行任务,下一次任务的开始执行时间是以上一次任务执行的开始时间+延时时间作为开始执行时间的;
使用service.schedule
执行任务:
public class ThreadPoolDemo49 {
public static void main(String[] args) {
//创建一个执行定时任务的线程池
ScheduledExecutorService service =
Executors.newScheduledThreadPool(10);
System.out.println("执行任务之前:" + new Date());
//执行任务
service.schedule(new Runnable() {
@Override
public void run() {
System.out.println("执行任务:" + new Date());
}
}, 3, TimeUnit.SECONDS);//3秒后执行一次
}
}
该代码的执行结果如下:
service.schedule
的区别:
- 没有延迟执行的时间设置。
- 定时任务只能执行一次
适用场景:任务定时启动一次
线程池的创建方式④:创建单一执行定时任务的线程池
与 线程池的创建方式③类似,这个创建方式,也有三种任务执行方式,我是用service.scheduleAtFixedRate
开始任务执行的,示例代码如下:
public class ThreadPoolDemo51 {
public static void main(String[] args) {
//创建单个执行定时任务的线程池
ScheduledExecutorService service =
Executors.newSingleThreadScheduledExecutor();
//开始定时任务
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("执行任务:" + new Date());
}
}, 1, 3, TimeUnit.SECONDS);
}
}
该代码的执行结果如下:
单一执行定时任务的线程池有什么意义
- 无需频繁的创建和销毁线程。
- 可以更好地分配、管理和存储任务(它有任务队列)。
线程池的创建方式⑤:创建单一线程的线程池
创建单一线程的线程池执行任务,示例代码如下:
public class ThreadPoolDemo52 {
public static void main(String[] args) {
//创建单一线程的线程池
ExecutorService service =
Executors.newSingleThreadExecutor();
//执行任务
for (int i = 0; i < 5; i++) {
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程名:" + Thread.currentThread().getName());
}
});
}
}
}
该代码的执行结果如下;
单一执行任务的线程池有什么意义
- 无需频繁的创建和销毁线程。
- 可以更好地分配、管理和存储任务(它有任务队列)。
线程池的创建方式⑥(JDK8+):根据当前的工作环境(CPU核心数、任务量)异步线程池
异步执行流程:
- main 调用异步线程池
- 异步线程池执行。
- 对于 main 线程来说,异步线程池已经执行完成,关闭 main 线程
示例代码如下:
public class ThreadPoolDemo53 {
public static void main(String[] args) {
//根据当前工作环境来创建线程池
ExecutorService service =
Executors.newWorkStealingPool();
for (int i = 0; i < 5; i++) {
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程名:" +
Thread.currentThread().getName());
}
});
}
}
}
该代码的执行结果如下:
对上述代码做出如下修改,即可看到异步线程池的运行效果:
public class ThreadPoolDemo53 {
public static void main(String[] args) {
//根据当前工作环境来创建线程池
ExecutorService service =
Executors.newWorkStealingPool();
for (int i = 0; i < 5; i++) {
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程名:" +
Thread.currentThread().getName());
}
});
}
//等待异步线程池执行完成(根据线程池的终止状态)
while (!service.isTerminated()) {
}
}
}
该代码的执行结果如下:
此线程池的创建方式的优点
- 执行速度非常快。
- 无须手动设置参数。
在《Java开发手册》中提到以下规则:
Executors 创建线程池的问题
- 线程数量不可控(造成线程的过度切换和争抢,过度消耗资源)
- 任务数量不可控(任务数量无限大,当任务量比较大的情况下就会造成OOM<内存溢出异常>)
以上的六种方法都使用了 Executors 去创建,所以我们需要去使用线程池的第七种创建方式。
线程池的创建方式⑦:new ThreadPoolExecutor
规避资源过度消耗,使用此创建线程池的方法,示例代码如下:
public class ThreadPoolDemo55 {
public static void main(String[] args) {
//原始创建线程池的方法
ThreadPoolExecutor executor =
new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000));
//第一个参数为核心线程数;第二个参数为最大线程数(一定大于等于核心线程数)
//第三个参数为最大线程数中的非核心线程的生存周期;第四个参数为第三个参数的时间单位
//第五个参数为任务队列,一定要设置容量
for (int i = 0; i < 5; i++) {
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程名:" + Thread.currentThread().getName());
}
});
}
}
}
该代码的执行结果:
使用此方法创建自定义线程池行为,示例代码如下:
public class ThreadPoolDemo57 {
private static int count = 1;
public static void main(String[] args) {
ThreadFactory threadFactory = new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("myThreadPool-" + count++);
return t;
}
};
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, 5, 0, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000), threadFactory
);
//执行任务
for (int i = 0; i < 5; i++) {
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程名:" + Thread.currentThread().getName());
}
});
}
}
}
该代码的执行结果如下:
拒绝策略(4+1)
JDK提供四种拒绝策略,还可以自定义拒绝策略。
JDK提供的四种拒绝策略+用户自定义拒绝策略:
- 默认的拒绝策略
- 自定义拒绝策略
JDK提供的四种拒绝策略:
①默认的拒绝策略,示例代码如下:
public class ThreadPoolDemo59 {
public static void main(String[] args) {
//创建线程池
ThreadPoolExecutor executor =
new ThreadPoolExecutor(5, 5, 0, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(5), new ThreadPoolExecutor.AbortPolicy());
for (int i = 0; i < 11; i++) {
int finalI = i;
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println("任务序列号" + finalI +
",线程名:" + Thread.currentThread().getName());
}
});
}
}
}
该代码的执行结果如下:
②使用调用线程池的线程来执行而任务
public class ThreadPoolDemo59 {
public static void main(String[] args) {
//创建线程池
ThreadPoolExecutor executor =
new ThreadPoolExecutor(5, 5, 0, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(5), new ThreadPoolExecutor.CallerRunsPolicy());
for (int i = 0; i < 11; i++) {
int finalI = i;
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println("任务序列号" + finalI +
",线程名:" + Thread.currentThread().getName());
}
});
}
}
}
该代码的执行结果如下:
③忽略(新)任务,示例代码如下:
public class ThreadPoolDemo59 {
public static void main(String[] args) {
//创建线程池
ThreadPoolExecutor executor =
new ThreadPoolExecutor(5, 5, 0, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(5), new ThreadPoolExecutor.DiscardPolicy());
for (int i = 0; i < 11; i++) {
int finalI = i;
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println("任务序列号" + finalI +
",线程名:" + Thread.currentThread().getName());
}
});
}
}
}
该代码的执行结果如下:
④忽略(旧)任务,示例代码如下:
public class ThreadPoolDemo59 {
public static void main(String[] args) {
//创建线程池
ThreadPoolExecutor executor =
new ThreadPoolExecutor(5, 5, 0, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(5), new ThreadPoolExecutor.DiscardOldestPolicy());
for (int i = 0; i < 11; i++) {
int finalI = i;
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println("任务序列号" + finalI +
",线程名:" + Thread.currentThread().getName());
}
});
}
}
}
该代码的执行结果如下:
⑤自定义拒绝策略,示例代码如下:
public class ThreadPoolDemo60 {
public static void main(String[] args) {
//创建线程池
ThreadPoolExecutor executor =
new ThreadPoolExecutor(5, 5, 0, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(5), new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
//自定义拒绝策略
System.out.println("执行了自定义拒绝策略");
}
});
for (int i = 0; i < 11; i++) {
int finalI = i;
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println("任务序列号" + finalI +
",线程名:" + Thread.currentThread().getName());
}
});
}
}
}
该代码的执行结果如下:
注意:
当任务数大于核心线程数的时候,多于的任务会被放在任务队列中,示例代码如下:
public class ThreadPoolDemo56 {
public static void main(String[] args) {
//原始创建线程池的方法
ThreadPoolExecutor executor =
new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000));
for (int i = 0; i < 6; i++) {
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程名:" + Thread.currentThread().getName());
}
});
}
}
}
该代码的执行结果如下:
可见,当任务数大于核心线程数的时候,多于的任务会被放在任务队列中。
线程池的执行流程
线程池的执行方式
线程池的执行方法:
executor.execute(new Runnable())
无返回值的线程池执行方法executor.submit(new Runnable())
无返回值的线程池执行方法executor.submit(new Callable<T>())
有返回值的线程池执行方法
executor.submit(new Callable<T>())
有返回值的线程池执行方法,示例代码如下:
public class ThreadPoolDemo61 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, 5, 5, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000));
//返回返回值
Future<Integer> future = executor.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int num = new Random().nextInt(10);
System.out.println("线程池生成了随机数:" + num);
return num;
}
});
System.out.println("main 得到返回值:" + future.get());
}
}
该代码的执行结果如下:
线程池执行方法的区别:
- execute 只能执行 Runnable 任务,并且是无返回值的; submit 既能执行 Runnable 无返回值的任务,也能执行 Callable 有返回值的任务。
- execute 执行任务,如果有 OOM 异常,会将异常打印到控制台; submit 执行任务出现了 OOM 异常,不会打印到控制台。
线程池的特征
线程池相比于线程来说是长生命周期的,即使没有任务了,也会运行并等待任务。
线程池的关闭
线程池的关闭方法:
executor.shutdown();
关闭线程池。executor.shutdownNow();
关闭线程池。
使用executor.shutdown();
关闭线程池,示例代码如下:
public class ThreadPoolDemo63 {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10, 10, 0, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000)
);
for (int i = 0; i < 15; i++) {
int finalI = i;
executor.submit(new Runnable() {
@Override
public void run() {
System.out.println(String.format("编号:%d,线程名:%s",
finalI, Thread.currentThread().getName()));
}
});
}
//正常关闭
executor.shutdown();
}
}
该代码的执行结果:
使用executor.shutdownNow();
关闭线程池,示例代码如下:
public class ThreadPoolDemo63 {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10, 10, 0, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000)
);
for (int i = 0; i < 15; i++) {
int finalI = i;
executor.submit(new Runnable() {
@Override
public void run() {
System.out.println(String.format("编号:%d,线程名:%s",
finalI, Thread.currentThread().getName()));
}
});
}
executor.shutdownNow();
}
}
该代码的执行结果如下:
两种线程池关闭方法的区别:
executor.shutdown();
关闭线程池,它会拒绝新任务的加入,等待线程池中的任务队列执行完之后,再停止线程池,线程池会进入 SHUTDOWN 状态。executor.shutdownNow();
关闭线程池,它会拒绝新任务的加入,不会等待任务队列中的任务执行完成,就停止线程池,线程池会进入 STOP 状态。
线程池的状态
注意:线程池的状态和线程的状态是不同的。
线程池的五个状态:
从源码和上图,我们可以看出,线程池一共存在5种状态,分别为:
- RUNNING:线程池创建之后的初始状态,这种状态下可以执行任务。
- SHUTDOWN:该状态下线程池不再接收新任务,但是会将工作队列中的任务执行结束。
- STOP:状态下线程池不再接收新任务,也不会处理工作队列中的任务,并且将会中断线程。
- TIDYING:该状态下所有任务都已终止,将会执行
terminated()
钩子方法。 - TERMINATED:执行完
terminated()
钩子方法之后。
线程池的状态只是给开发者使用的,对于客户机是不可见的。
线程池的应用
需求1:使用最高效的方式实现两个Date(时间类型)的格式化。
实现该需求代码如下:
public class ThreadPoolDemo64 {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
Date date = new Date(1000);
//执行时间格式化并打印结果
myFormatTime(date);
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
Date date = new Date(2000);
//执行时间格式化并打印结果
myFormatTime(date);
}
});
t1.start();
t2.start();
}
private static void myFormatTime(Date date) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
String result = simpleDateFormat.format(date);
System.out.println("时间:" + result);
}
}
该代码执行结果如下:
需求2:使用最高效的方式实现 10 个时间的格式化。
实现该需求代码如下:
public class ThreadPoolDemo65 {
public static void main(String[] args) {
for (int i = 1; i < 11; i++) {
final int finalI = i;
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
Date date = new Date(finalI * 1000);
//执行时间格式化并打印结果
myFormatTime(date);
}
});
t1.start();
}
}
private static void myFormatTime(Date date) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
String result = simpleDateFormat.format(date);
System.out.println("时间:" + result);
}
}
该代码的执行结果如下:
需求3:使用最高效的方式实现 1000 个时间的格式化。
public class ThreadPoolDemo66 {
static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
public static void main(String[] args) {
ThreadPoolExecutor executor =
new ThreadPoolExecutor(10, 10, 0,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
for (int i = 1; i < 1001; i++) {
final int finalI = i;
executor.execute(new Runnable() {
@Override
public void run() {
Date date = new Date(finalI * 1000);
//执行时间格式化并打印结果
myFormatTime(date);
}
});
}
}
private static void myFormatTime(Date date) {
String result = simpleDateFormat.format(date);
System.out.println("时间:" + result);
}
}
该代码的执行结果如下:
我们发现,该程序出现了线程不安全的问题。
解决线程不安全:
- 加锁:虽然可以解决线程的不安全问题,同时也带来了新的问题:排队执行引起性能过度消耗。
- ThreadLocal:线程级别的私有变量,即可解决线程不安全问题,又不会将任务排队进行。
使用加锁的方式,实现此需求:
public class ThreadPoolDemo66 {
static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
public static void main(String[] args) {
ThreadPoolExecutor executor =
new ThreadPoolExecutor(10, 10, 0,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
for (int i = 1; i < 1001; i++) {
final int finalI = i;
executor.execute(new Runnable() {
@Override
public void run() {
Date date = new Date(finalI * 1000);
//执行时间格式化并打印结果
myFormatTime(date);
}
});
}
executor.shutdown();
}
private static synchronized void myFormatTime(Date date) {
String result = simpleDateFormat.format(date);
System.out.println("时间:" + result);
}
}
该代码的执行结果为:
使用ThreadLocal
方法的方式,实现此需求:
public class ThreadPoolDemo73 {
static ThreadLocal<SimpleDateFormat> threadLocal =
ThreadLocal.withInitial(() ->
new SimpleDateFormat("mm:ss"));
// ThreadLocal.withInitial(new Supplier<SimpleDateFormat>() {
// @Override
// public SimpleDateFormat get() {
// System.out.println("----------执行初始化方法----------");
// return new SimpleDateFormat("mm:ss");
// }
// });
public static void main(String[] args) {
ThreadPoolExecutor executor =
new ThreadPoolExecutor(10, 10, 0,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000)
);
for (int i = 1; i < 1001; i++) {
final int finalI = i;
executor.execute(new Runnable() {
@Override
public void run() {
Date date = new Date(finalI * 1000);
myFormat(date);
}
});
}
}
public static void myFormat(Date date) {
String result = threadLocal.get().format(date);
System.out.println("时间:" + result);
}
}
该代码的执行结果如下:
我们发现,完成需求的同时线程不安全的问题得到解决。
ThreadLocal 的使用
ThreadLocal的使用场景:
- 解决线程不安全的问题。
- 实现线程级别的数据传递
ThreadLocal 中有三个重要的方法
- set(T):将变量存放到线程中。
- get():从线程中取得私有变量。
- remove():从线程中移除私有变量。
ThreadLocal 三个重要方法的使用,示例代码如下:
public class ThreadPoolDemo67 {
// 创建 ThreadLocal
static ThreadLocal<String> threadLocal =
new ThreadLocal<>();
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
// set threadLocal
String tname = Thread.currentThread().getName();
threadLocal.set(tname);
System.out.println(String.format("线程:%s 设置了值:%s",
tname, tname));
printTName();
} finally {
threadLocal.remove();
}
}
}, "线程1");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
String tname = Thread.currentThread().getName();
// set ThreadLocal
threadLocal.set(tname);
System.out.println(String.format("线程:%s 设置了值:%s",
tname, tname));
printTName();
} finally {
threadLocal.remove();
}
}
}, "线程2");
t2.start();
}
private static void printTName() {
String tname = Thread.currentThread().getName();
// 得到存放在 ThreadLoca 中的值
String result = threadLocal.get();
System.out.println(String.format("线程:%s,取的:%s",
tname, result));
}
}
该代码的执行结果如下:
initialValue()
初始化方法的使用,示例代码如下:
public class ThreadPoolDemo68 {
// 创建并初始化
static ThreadLocal<String> threadLocal =
new ThreadLocal() {
@Override
protected String initialValue() {
System.out.println("执行了初始化方法");
return "Java";
}
};
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
1, 1, 0, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000)
);
for (int i = 0; i < 2; i++) {
executor.execute(new Runnable() {
@Override
public void run() {
String result = threadLocal.get();
System.out.println("得到数据:" + result);
}
});
}
}
}
该代码的执行结果如下:
注意:initalValue 返回数据类型一定要和 ThreadLocal 定义的泛型类型保持一致。
观察下面代码
public class ThreadPoolDemo69 {
static ThreadLocal<String> threadLocal = new ThreadLocal() {
@Override
protected Object initialValue() {
System.out.println("执行了初始化方法");
return "Java";
}
};
public static void main(String[] args) {
threadLocal.set("MySql");
String result = threadLocal.get();
System.out.println("结果:" + result);
}
}
该代码的执行结果如下:
通过这个例子,我们可以发现initialValue
+set
+get
的存取操作为:先执行 set
方法,再执行get
方法,initialValue
没有执行。其原因为 ThreadLocal 是懒加载的,当调用了 get 方法之后,才会尝试执行 initialValue (初始化)方法,尝试获取一下 ThreadLocal 的 set 值,如果获取到了值,那么初始化方法永远不会执行。原码如下:
withInitial()
初始化方法的使用,示例代码如下:
public class ThreadPoolDemo70 {
static ThreadLocal<String> threadLocal = ThreadLocal.withInitial(new Supplier<String>() {
@Override
public String get() {
System.out.println("执行了初始化方法");
return "Java";
}
});
public static void main(String[] args) {
String result = threadLocal.get();
System.out.println("结果:" + result);
}
}
该代码的执行结果如下:
withInitial()`初始化方法可以配合JDK8+中的 Lambda 表达式一起使用,示例代码如下:
public class ThreadPoolDemo71 {
static ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "Java");
public static void main(String[] args) {
try {
String result = threadLocal.get();
System.out.println("结果:" + result);
} finally {
threadLocal.remove();
}
}
}
该代码的执行结果如下:
ThreadLocal 的缺点
ThreadLocal 的缺点:
- 不可继承
- 脏读(脏数据)
- 内存溢出问题(最常出现的问题)、打开数据连接但未关闭。
ThreadLocal 不可继承性
子线程中不能读取到父线程的值,示例代码如下:
/*
* 缺点1:不可继承
*/
public class ThreadLocal75 {
static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
// set ThrealLocal
threadLocal.set("Java");
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// get ThreadLocal
String result = threadLocal.get();
System.out.println("结果:" + result);
}
});
t1.start();
}
}
该代码的执行结果如下:
我们发现:打印在控制台的结果,并不是Java
而是null
,体现了ThreadLocal
不可继承。
解决ThreadLocal
不可继承性——使用InheritableThreadLocal<>()
,示例代码如下:
public class ThreadPoolDemo76 {
static ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
public static void main(String[] args) {
// set ThrealLocal
threadLocal.set("Java");
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// get ThreadLocal
String result = threadLocal.get();
System.out.println("结果:" + result);
}
});
t1.start();
}
}
该代码的执行结果如下:
我们发现:此时控制台的打印的结果为Java
。
注意事项:InheritableThreadLocal
不能实现不同线程之间的数据共享。
ThreadLocal 脏读(脏数据)
在一个线程中读取到了不属于自己的数据。
线程中用ThreadLocal
不会出现脏读(每个线程都使用的是自己的变量值和ThreadLocal
)。
线程池里使用ThreadLocal
就会出现脏数据,线程池会复用线程,复用线程之后,也会复用线程中的静态属性,从而导致某些方法不能被执行,于是就出现了脏数据的问题。
首先看一个正常情况的示例代码:
/*
* 正常情况(未出现脏数据)
*/
public class ThreadPoolDemo78 {
static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
// 执行线程 1
MyTask t1 = new MyTask();
t1.start();
// 执行线程 2
MyTask t2 = new MyTask();
t2.start();
}
static class MyTask extends Thread {
// 标识是否第一次访问
static boolean first = true;
@Override
public void run() {
if (first) {
// 第一次访问,存储用户信息
threadLocal.set(this.getName() + " session info.");
first = false;
}
// get threadLocal
String result = threadLocal.get();
System.out.println(this.getName() + ":" + result);
}
}
}
该代码的执行结果如下:
ThreadLocal
脏读(脏数据)示例代码如下:
public class ThreadPoolDemo79 {
static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
ThreadPoolExecutor executor =
new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000));
for (int i = 0; i < 2; i++) {
MyTask t = new MyTask();
executor.execute(t);
}
}
static class MyTask extends Thread {
// 标识是否第一次访问
static boolean first = true;
@Override
public void run() {
if (first) {
// 第一次访问,存储用户信息
threadLocal.set(this.getName() + " session info.");
first = false;
}
// get threadLocal
String result = threadLocal.get();
System.out.println(this.getName() + ":" + result);
}
}
}
该代码的执行结果如下:
我们发现:Thread-0
中读取的Thread-0
的信息;而Thread-1
中读取的也是Thread-0
的信息,此时出现脏数据。
脏数据的解决方案:
- 避免使用静态属性(静态属性在线程池中会复用)。
- 使用
remove
解决。
解决脏数据问题,示例代码如下:
public class ThreadPoolDemo79 {
static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
ThreadPoolExecutor executor =
new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000));
for (int i = 0; i < 2; i++) {
MyTask t = new MyTask();
executor.execute(t);
}
}
static class MyTask extends Thread {
// 标识是否第一次访问
static boolean first = true;
@Override
public void run() {
if (first) {
// 第一次访问,存储用户信息
threadLocal.set(this.getName() + " session info.");
first = false;
}
try {
// get threadLocal
String result = threadLocal.get();
System.out.println(this.getName() + ":" + result);
} finally {
threadLocal.remove();
}
}
}
}
该代码的执行结果如下:
我们发现脏数据的问题得到解决。
ThreadLocal 内存溢出问题、打开数据连接但未关闭
内存溢出:当一个线程执行完之后,不会释放这个线程所占用内存,或者释放内存不及时的情况都叫做内存溢出(线程不用了,但线程相关的内存还得不到及时地释放)。
示例代码如下:
public class ThreadPoolDemo80 {
static ThreadLocal<MyBigClass> threadLocal = new ThreadLocal<>();
// 1mb 的对象
static class MyBigClass {
private byte[] bytes = new byte[1 * 1024 * 1024];
}
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10,
0, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100));
for (int i = 0; i < 5; i++) {
int finalI = i;
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() +
",编号:" + finalI + "执行了存储方法。");
MyBigClass myBigClass = new MyBigClass();
threadLocal.set(myBigClass);
myBigClass = null;
}
});
}
}
}
运行此程序时,我们需要设置它的最大使用内存,方便我们观察。
该代码的执行结果如下:
我们发现,此程序出现了OOM异常。
使用 remove()解决 OOM 异常,示例代码如下:
public class ThreadLocalDemo83 {
// 1m 大小的对象
static class OOMObject {
private byte[] bytes = new byte[1 * 1024 * 1024];
}
static ThreadLocal<OOMObject> threadLocal = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10, 0,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
for (int i = 0; i < 5; i++) {
int finalI = i;
executor.execute(new Runnable() {
@Override
public void run() {
try {
OOMObject oomObject = new OOMObject();
System.out.println("任务:" + finalI + ",执行了");
threadLocal.set(oomObject);
oomObject = null;
} finally {
threadLocal.remove();
}
}
});
Thread.sleep(200);
}
}
}
该代码的执行结果如下:
以上是ThreadLocal 主要的三个缺点。
Hashmap 和 ThreadLocalMap 处理 hash 冲突的区别:
Hashmap 使用的是链表法,而 ThreadLocalMap 使用的是开放寻址法。
为什么要这样实现:
开放寻址法的特点和使用场景是数据量比较少的情况下性能更好;而 HashMap 里面存错的数据通常情况下是比较多的,这个时候开放寻址法效率比较低,所以使用链表法。