threadlocal的缺点,threadlocal应用场景

首页 > 技术 > 作者:YD1662023-04-15 23:39:40

在 Java 中,如果要问哪个类使用简单,但用好最不简单?我想你的脑海中一定会浮现出一次词——“ThreadLocal”。

确实如此,threadLocal 原本设计是为了解决并发时,线程共享变量的问题,但由于过度设计,如弱引用和哈希碰撞,从而导致它的理解难度大和使用成本高等问题。当然,如果稍有不慎还是导致脏数据、内存溢出、共享变量更新等问题,但即便如此,ThreadLocal 依旧有适合自己的使用场景,以及无可取代的价值,比如本文要介绍了这两种使用场景,除了 ThreadLocal 之外,还真没有合适的替代方案。

使用场景1:本地变量

我们以多线程格式化时间为例,来演示 ThreadLocal 的价值和作用,当我们在多个线程中格式化时间时,通常会这样操作。

① 2个线程格式化

当有 2 个线程进行时间格式化时,我们可以这样写:

import java.text.SimpleDateFormat; import java.util.Date; public class Test { public static void main(String[] args) throws InterruptedException { // 创建并启动线程1 Thread t1 = new Thread(new Runnable() { @Override public void run() { // 得到时间对象 Date date = new Date(1 * 1000); // 执行时间格式化 formatAndPrint(date); } }); t1.start(); // 创建并启动线程2 Thread t2 = new Thread(new Runnable() { @Override public void run() { // 得到时间对象 Date date = new Date(2 * 1000); // 执行时间格式化 formatAndPrint(date); } }); t2.start(); } /** * 格式化并打印结果 * @param date 时间对象 */ private static void formatAndPrint(Date date) { // 格式化时间对象 SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss"); // 执行格式化 String result = simpleDateFormat.format(date); // 打印最终结果 System.out.println("时间:" result); } }

以上程序的执行结果为:

threadlocal的缺点,threadlocal应用场景(1)

上面的代码因为创建的线程数量并不多,所以我们可以给每个线程创建一个私有对象 SimpleDateFormat 来进行时间格式化。

② 10个线程格式化

当线程的数量从 2 个升级为 10 个时,我们可以使用 for 循环来创建多个线程执行时间格式化,具体实现代码如下:

import java.text.SimpleDateFormat; import java.util.Date; public class Test { public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 10; i ) { int finalI = i; // 创建线程 Thread thread = new Thread(new Runnable() { @Override public void run() { // 得到时间对象 Date date = new Date(finalI * 1000); // 执行时间格式化 formatAndPrint(date); } }); // 启动线程 thread.start(); } } /** * 格式化并打印时间 * @param date 时间对象 */ private static void formatAndPrint(Date date) { // 格式化时间对象 SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss"); // 执行格式化 String result = simpleDateFormat.format(date); // 打印最终结果 System.out.println("时间:" result); } }

以上程序的执行结果为:

threadlocal的缺点,threadlocal应用场景(2)

从上述结果可以看出,虽然此时创建的线程数和 SimpleDateFormat 的数量不算少,但程序还是可以正常运行的。

③ 1000个线程格式化

然而当我们将线程的数量从 10 个变成 1000 个的时候,我们就不能单纯的使用 for 循环来创建 1000 个线程的方式来解决问题了,因为这样频繁的新建和销毁线程会造成大量的系统开销和线程过度争抢 CPU 资源的问题。

所以经过一番思考后,我们决定使用线程池来执行这 1000 次的任务,因为线程池可以复用线程资源,无需频繁的新建和销毁线程,也可以通过控制线程池中线程的数量来避免过多线程所导致的 CPU 资源过度争抢和线程频繁切换所造成的性能问题,而且我们可以将 SimpleDateFormat 提升为全局变量,从而避免每次执行都要新建 SimpleDateFormat 的问题,于是我们写下了这样的代码:

import java.text.SimpleDateFormat; import java.util.Date; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class App { // 时间格式化对象 private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss"); public static void main(String[] args) throws InterruptedException { // 创建线程池执行任务 ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000)); for (int i = 0; i < 1000; i ) { int finalI = i; // 执行任务 threadPool.execute(new Runnable() { @Override public void run() { // 得到时间对象 Date date = new Date(finalI * 1000); // 执行时间格式化 formatAndPrint(date); } }); } // 线程池执行完任务之后关闭 ThreadPool.shutdown(); } /** * 格式化并打印时间 * @param date 时间对象 */ private static void formatAndPrint(Date date) { // 执行格式化 String result = simpleDateFormat.format(date); // 打印最终结果 System.out.println("时间:" result); } }

以上程序的执行结果为:

threadlocal的缺点,threadlocal应用场景(3)

当我们怀着无比喜悦的心情去运行程序的时候,却发现意外发生了,这样写代码竟然会出现线程安全的问题。从上述结果可以看出,程序的打印结果竟然有重复内容的,正确的情况应该是没有重复的时间才对。

PS:所谓的线程安全问题是指:在多线程的执行中,程序的执行结果与预期结果不相符的情况

a) 线程安全问题分析

为了找到问题所在,我们尝试查看 SimpleDateFormat 中 format 方法的源码来排查一下问题,format 源码如下:

private StringBuffer format(Date date, StringBuffer toAppendTo, FieldDelegate delegate) { // 注意此行代码 calendar.setTime(date); boolean useDateFormatSymbols = useDateFormatSymbols(); for (int i = 0; i < compiledPattern.length; ) { int tag = compiledPattern[i] >>> 8; int count = compiledPattern[i ] & 0xff; if (count == 255) { count = compiledPattern[i ] << 16; count |= compiledPattern[i ]; } switch (tag) { case TAG_QUOTE_ASCII_CHAR: toAppendTo.append((char)count); break; case TAG_QUOTE_CHARS: toAppendTo.append(compiledPattern, i, count); i = count; break; default: subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols); break; } } return toAppendTo; }

从上述源码可以看出,在执行 SimpleDateFormat.format 方法时,会使用 calendar.setTime 方法将输入的时间进行转换,那么我们想象一下这样的场景:

  1. 线程 1 执行了 calendar.setTime(date) 方法,将用户输入的时间转换成了后面格式化时所需要的时间;
  2. 线程 1 暂停执行,线程 2 得到 CPU 时间片开始执行;
  3. 线程 2 执行了 calendar.setTime(date) 方法,对时间进行了修改;
  4. 线程 2 暂停执行,线程 1 得出 CPU 时间片继续执行,因为线程 1 和线程 2 使用的是同一对象,而时间已经被线程 2 修改了,所以此时当线程 1 继续执行的时候就会出现线程安全的问题了。

正常的情况下,程序的执行是这样的:

threadlocal的缺点,threadlocal应用场景(4)

首页 12345下一页

栏目热文

文档排行

本站推荐

Copyright © 2018 - 2021 www.yd166.com., All Rights Reserved.