本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!前端
接下来将承接上文,介绍ThreadLocal的另外一个经典使用场景后端
上一遍文章连接:ThreadLocal经典使用场景安全
ThreadLocal 用做保存每一个线程独享的对象,为每一个线程都建立一个副本,这样每一个线程均可以修改本身所拥有的副本, 而不会影响其余线程的副本,确保了线程安全。markdown
前几天我在网上看到一篇介绍,写的确实不错,咱们一块儿来学习下。多线程
假设咱们有 10 个线程同时对应 10 个 SimpleDateFormat 对象。看输出什么,咱们来看下面这种写法:并发
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
int finalI = i;
new Thread(() -> {
String date = new ThreadLocalDemo02().date(finalI);
System.out.println(date);
}).start();
Thread.sleep(100);
}
}
public String date(int seconds) {
Date date = new Date(1000 * seconds);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
return simpleDateFormat.format(date);
}
}
复制代码
上面的代码利用了一个 for 循环来完成这个需求。for 循环一共循环 10 次,每一次都会新建一个线程,而且每个线程都会在 date 方法中建立一个 SimpleDateFormat 对象,示意图以下:ide
能够看出一共有 10 个线程,对应 10 个 SimpleDateFormat 对象。post
代码的运行结果:学习
可是线程不能无休地建立下去,由于线程越多,所占用的资源也会越多。假设咱们须要 1000 个任务,那就不能再用 for 循环的方法了,而是应该使用线程池来实现线程的复用,不然会消耗过多的内存等资源。测试
利用线程池:
public static ExecutorService threadPool = Executors.newFixedThreadPool(16);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalDemo03().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = new SimpleDateFormat("mm:ss");
return dateFormat.format(date);
}
}
复制代码
能够看出,咱们用了一个 16 线程的线程池,而且给这个线程池提交了 1000 次任务。每一个任务中它作的事情和以前是同样的,仍是去执行 date 方法,而且在这个方法中建立一个 simpleDateFormat 对象。程序的一种运行结果是(多线程下,运行结果不惟一):
00:00
00:07
00:04
00:02
...
16:29
16:28
16:27
16:26
16:39
复制代码
程序运行结果正确,把从 00:00 到 16:39 这 1000 个时间给打印了出来,而且没有重复的时间。咱们把这段代码用图形化给表示出来,如图所示:
图的左侧是一个线程池,右侧是 1000 个任务。咱们刚才所作的就是每一个任务都建立了一个 simpleDateFormat 对象,也就是说,1000 个任务对应 1000 个 simpleDateFormat 对象。
可是这样作是没有必要的,由于这么多对象的建立是有开销的,而且在使用完以后的销毁一样是有开销的,并且这么多对象同时存在在内存中也是一种内存的浪费。
如今咱们就来优化一下。既然不想要这么多的 simpleDateFormat 对象,最简单的就是只用一个就能够了
咱们用下面的代码来演示只用一个 simpleDateFormat 对象的状况:
public static ExecutorService threadPool = Executors.newFixedThreadPool(16);
static SimpleDateFormat dateFormat = new SimpleDateFormat("mm:ss");
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalDemo04().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
Date date = new Date(1000 * seconds);
return dateFormat.format(date);
}
}
复制代码
在代码中能够看出,其余的没有变化,变化之处就在于,咱们把这个 simpleDateFormat 对象给提取了出来,变成 static 静态变量,须要用的时候直接去获取这个静态对象就能够了。看上去省略掉了建立 1000 个 simpleDateFormat 对象的开销,看上去没有问题,咱们用图形的方式把这件事情给表示出来:
从图中能够看出,咱们有不一样的线程,而且线程会执行它们的任务。可是不一样的任务所调用的 simpleDateFormat 对象都是同一个,因此它们所指向的那个对象都是同一个,可是这样一来就会有线程不安全的问题。
00:04
00:04
00:05
00:04
...
16:15
16:14
16:13
复制代码
执行上面的代码就会发现,控制台所打印出来的和咱们所期待的是不一致的。咱们所期待的是打印出来的时间是不重复的,可是能够看出在这里出现了重复,好比第一行和第二行都是 04 秒,这就表明它内部已经出错了。
出错的缘由就在于,simpleDateFormat 这个对象自己不是一个线程安全的对象,不该该被多个线程同时访问。因此咱们就想到了一个解决方案,用 synchronized 来加锁。因而代码就修改为下面的样子:
public static ExecutorService threadPool = Executors.newFixedThreadPool(16);
static SimpleDateFormat dateFormat = new SimpleDateFormat("mm:ss");
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalDemo05().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
Date date = new Date(1000 * seconds);
String s = null;
synchronized (ThreadLocalDemo05.class) {
s = dateFormat.format(date);
}
return s;
}
}
复制代码
能够看出在 date 方法中加入了 synchronized 关键字,把 simpleDateFormat 的调用给上了锁。
00:00
00:01
00:06
...
15:56
16:37
16:36
复制代码
这样的结果是正常的,没有出现重复的时间。可是因为咱们使用了 synchronized 关键字,就会陷入一种排队的状态,多个线程不能同时工做,这样一来,总体的效率就被大大下降了。有没有更好的解决方案呢?
咱们但愿达到的效果是,既不浪费过多的内存,同时又想保证线程安全。通过思考得出,可让每一个线程都拥有一个本身的 simpleDateFormat 对象来达到这个目的,这样就能一箭双鵰了。
那么,要想达到上面所说目的,咱们就可使用 ThreadLocal。示例代码以下所示:
public class ThreadLocalDemo06 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(16);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalDemo06().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
return dateFormat.format(date);
}
}
class ThreadSafeFormatter {
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("mm:ss");
}
};
}
复制代码
在这段代码中,咱们使用了 ThreadLocal 帮每一个线程去生成它本身的 simpleDateFormat 对象,对于每一个线程而言,这个对象是独享的。但与此同时,这个对象就不会创造过多,一共只有 16 个,由于线程只有 16 个。
00:05
00:04
00:01
...
16:37
16:36
16:32
复制代码
这个结果是正确的,不会出现重复的时间。
咱们用图来看一下当前的这种状态:
在图中的左侧能够看到,这个线程池一共有 16 个线程,对应 16 个 simpleDateFormat 对象。而在这个图画的右侧是 1000 个任务,任务是很是多的,和原来同样有 1000 个任务。可是这里最大的变化就是,虽然任务有 1000 个,可是咱们再也不须要去建立 1000 个 simpleDateFormat 对象了。即使任务再多,最终也只会有和线程数相同的 simpleDateFormat 对象。这样既高效地使用了内存,又同时保证了线程安全。
以上就是第一种很是典型的适合使用 ThreadLocal 的场景。
感谢您的阅读,欢迎点赞、评论、分享
学无止境、天天积累一点点。分享多一点点!