《Java高并发程序设计》学习 --4.3 ThreadLocal

2024-02-16 15:18

本文主要是介绍《Java高并发程序设计》学习 --4.3 ThreadLocal,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

3. ThreadLocal
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
下面看一个简单的示例:
        private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");public static class ParseDate implements Runnable {int i = 0;public ParseDate(int i) {this.i = i;}@Overridepublic void run() {try {Date t = sdf.parse("2015-03-12 12:29:"+i%60);System.out.println(i + ":" + t);} catch (ParseException e) {e.printStackTrace();}}}public static void main(String[] args) {ExecutorService es = Executors.newFixedThreadPool(10);for(int i=0; i<1000; i++) {es.execute(new ParseDate(i));}}

上述代码再多线程中使用SimpleDateFormat来解析字符串类型的日期。如果你执行上述代码,可能得到一些异常:
Exception in thread "pool-1-thread-13" java.lang.NumberFormatException: For input string: ""
java.lang.NumberFormatException: multiple points
出现这些问题的原因,是SimpleDateFormat.parse()方法并不是线程安全的。因此,在线程池中共享这个对象必然导致错误。
一种可行的方案是在 sdf.parse()前后加锁,这里使用ThreadLocal为每一个线程产生一个SimpleDateFormat对象实例:
static ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<SimpleDateFormat>();
public static class ParseDate implements Runnable {int i = 0;public ParseDate(int i) {this.i = i;}@Overridepublic void run() {try {if(tl.get() == null)tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));Date t = tl.get().parse("2015-03-12 12:29:"+i%60);System.out.println(i + ":" + t);} catch (ParseException e) {e.printStackTrace();}}
}
public static void main(String[] args) {ExecutorService es = Executors.newFixedThreadPool(10);for(int i=0; i<1000; i++) {es.execute(new ParseDate(i));}
}
上述代码中,如果当前线程不持有SimpleDateFormat对象实例。那么就新建一个并把它设置到当前线程中,如果已经持有,则直接使用。
为每一个线程分配一个对象的工作并不是由ThreadLocal来完成的,而是需要在应用层面保证的。如果在应用上为每一个线程分配了相同的对象实例,那么ThreadLocal也不能保证线程安全。

2)ThreadLocal的实现原理
ThreadLocal如何保证这些对象只被当前线程所访问,我们需要关注的是ThreadLocal的set()方法和get()方法。从set()方法说起:
public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);
}
在set时,首先获得当前线程对象,然后通过getMap()拿到线程的ThreadLocalMap,并将值设入ThreadLocalMap中。而ThreadLocalMap可以理解为一个Map,但是它是定义在Thread内部的成员:
ThreadLocal.ThreadLocalMap threadLocals = null;
而设置到ThreadLocal中的数据,也正是写入了threadLocals这个Map。其中,key为ThreadLocal当前对象,value就是我们需要的值。而threadLocals本身就保存了当前自己所在线程的所有“局部变量”,也就是一个ThreadLocal变量的集合。
在进行get()操作时,就是将这个Map中的数据拿出来:
public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null)return (T)e.value;}return setInitialValue();
}
首先,get()方法也是先取得当前线程的ThreadLocalMap对象。然后,通过将自己作为key取得内部的实际数据。
当线程退出时,Thread类会进行一些清理工作,其中就包括清理ThreadLocalMap:
private void exit() {if (group != null) {group.threadTerminated(this);group = null;}/* Aggressively null out all reference fields: see bug 4006245 */target = null;/* Speed the release of some of these resources */threadLocals = null;inheritableThreadLocals = null;inheritedAccessControlContext = null;blocker = null;uncaughtExceptionHandler = null;
}
如果使用线程池,意味着当前线程未必会退出(比如固定大小的线程池,线程总是存在)如果这样,将一些大大的对象设置到ThreadLocal中(它实际保存在线程持有的threadLocals Map内),可能会使系统出现内存泄漏的可能。
此时,如果希望及时回收对象,最好使用ThreadLocal.remove()方法将整个变量移除。
如果对于ThreadLocal的变量,手动将其设置为null,比如tl=null。那么这个ThreadLocal对应的所有线程的局部变量都有可能被回收。看一个简单的例子:
public class ThreadLocalDemo_Gc {static volatile ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<SimpleDateFormat>() {protected void finalize() throws Throwable {System.out.println(this.toString() + " is gc");}};static volatile CountDownLatch cd = new CountDownLatch(10000);public static class ParseDate implements Runnable {int i = 0;public ParseDate(int i) {this.i = i;}@Overridepublic void run() {try {if(tl.get() == null) {tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"){@Overrideprotected void finalize() throws Throwable {System.out.println(this.toString() + " is gc");}});System.out.println(Thread.currentThread().getId() + ":create SimpleDateFormat");}Date t = tl.get().parse("2015-03-29 19:29:" + i%60);} catch (ParseException e) {e.printStackTrace();} finally {cd.countDown();}}}public static void main(String[] args) throws InterruptedException {ExecutorService es = Executors.newFixedThreadPool(10);for(int i=0; i<10000; i++) {es.execute(new ParseDate(i));}cd.await();System.out.println("mission complete!!");tl = null;System.gc();System.out.println("first GC complete!!");//在设置ThreadLocal的时候,会清楚ThreadLocalMap中的无效对象tl = new ThreadLocal<SimpleDateFormat>();cd = new CountDownLatch(10000);for (int i = 0; i < 10000; i++) {es.execute(new ParseDate(i));}cd.await();Thread.sleep(1000);System.gc();System.out.println("second GC complete!!");}
}
在主函数main中,先后进行了两次任务提交,每次10000个任务。在第一次任务提交后,将tl设置为null,接着进行一次GC。接着,进行第二次任务提交,完成后,再进行一次GC。
执行上述代码,则最有可能的一种输出如下:
11:create SimpleDateFormat
9:create SimpleDateFormat
13:create SimpleDateFormat
14:create SimpleDateFormat
18:create SimpleDateFormat
12:create SimpleDateFormat
10:create SimpleDateFormat
16:create SimpleDateFormat
17:create SimpleDateFormat
15:create SimpleDateFormat
mission complete!!
first GC complete!!
cn.guet.parallel.ThreadLocalDemo_Gc$1@4d31477b is gc
11:create SimpleDateFormat
12:create SimpleDateFormat
15:create SimpleDateFormat
13:create SimpleDateFormat
18:create SimpleDateFormat
9:create SimpleDateFormat
17:create SimpleDateFormat
10:create SimpleDateFormat
16:create SimpleDateFormat
14:create SimpleDateFormat
second GC complete!!
cn.guet.parallel.ThreadLocalDemo_Gc$ParseDate$1@4f76f1a0 is gc
cn.guet.parallel.ThreadLocalDemo_Gc$ParseDate$1@4f76f1a0 is gc
cn.guet.parallel.ThreadLocalDemo_Gc$ParseDate$1@4f76f1a0 is gc
cn.guet.parallel.ThreadLocalDemo_Gc$ParseDate$1@4f76f1a0 is gc
cn.guet.parallel.ThreadLocalDemo_Gc$ParseDate$1@4f76f1a0 is gc
cn.guet.parallel.ThreadLocalDemo_Gc$ParseDate$1@4f76f1a0 is gc
cn.guet.parallel.ThreadLocalDemo_Gc$ParseDate$1@4f76f1a0 is gc
cn.guet.parallel.ThreadLocalDemo_Gc$ParseDate$1@4f76f1a0 is gc
cn.guet.parallel.ThreadLocalDemo_Gc$ParseDate$1@4f76f1a0 is gc
cn.guet.parallel.ThreadLocalDemo_Gc$ParseDate$1@4f76f1a0 is gc
首先,线程池中10个线程都各自创建了一个SimpleDateFormat对象实例。接着进行第一次GC,可以看到ThreadLocal对象被回收了。接着提交了第2次任务,这次一样也创建了10个SimpleDateFormat对象。然后,进行第2次GC。可以看到,在第2次GC后,第一次创建的10个SimpleDateFormat子类实例全部被回收。可以看到,虽然我们没有手工remove()这些对象,但是系统依然有可能回收它们。
ThreadLocalMap的实现使用了弱引用。弱引用是比强引用弱得多的引用。Java虚拟机在垃圾回收时,如果发现弱引用,就会立即回收。
ThreadLocalMap内部由一系列Entry构成,每一个Entry都是WeakReference<ThreadLocal>:
static class Entry extends WeakReference<ThreadLocal> {
	Object value;
	Entry(ThreadLocal k, Object v) {
		super(k);
		value = v;
	}
}
这里的参数k就是Map的key,v就是Map的value。其中k也就是ThreadLocal实例,作为弱引用使用。因此,虽然这里使用ThreadLocal作为Map的key,但实际上,它并不是真的持有ThreadLocal的引用。而当ThreadLocal的外部引用被回收时,ThreadLocalMap中的key就会变为null。当系统进行ThreadLocalMap清理时(比如将新的变量加入表中,就会自动进行一次清理),就会自然将这些垃圾数据回收。
下图是本文介绍到的一些对象之间的引用关系图,实线表示强引用,虚线表示弱引用:

3)对性能有何帮助
为每一个线程分配一个独立的对象对系统性能也许是有帮助的。这也不一定,这完全取决于共享对象的内部逻辑。如果共享对象对于竞争的处理容易引起性能损失,还是应该考虑使用ThreadLocal为每个线程分配单独的对象。一个典型的案例就是在多线程下产生随机数。
这里,简单测试一下在多线程下产生随机数的性能问题。首先,定义一些全局变量:
    public static final int GEN_COUNT = 10000000;public static final int THREAD_COUNT = 4;static ExecutorService exe = Executors.newFixedThreadPool(THREAD_COUNT);public static Random rnd = new Random(123);public static ThreadLocal<Random> tRnd = new ThreadLocal<Random>() {protected Random initialValue() {return new Random(123);}};
代码第1行定义了每个线程要产生的随机数数量,第2行定义了参与工作的线程数量,第3行定义了线程池,第4行定义了被多线程共享的Random实例用于产生随机数,第6~11行定义了有ThreadLocal封装的Random。
接着,定义一个工作线程的内部逻辑。它可以工作在两种模式下:
第一是多线程共享一个Random(mode=0),
第二是多个线程各分配一个Random(mode=1)。
public static class RndTask implements Callable<Long> {private int mode = 0;public RndTask(int mode) {this.mode = mode;}public Random getRandom() {if(mode == 0) {return rnd;} else if(mode == 1) {return tRnd.get();} else {return null;}}@Overridepublic Long call() throws Exception {long b = System.currentTimeMillis();for(long i=0; i<GEN_COUNT; i++) {getRandom().nextInt();}long e = System.currentTimeMillis();System.out.println(Thread.currentThread().getName() + " spend " + (e-b) + "ms");return e - b;}
}
上述代码第19~27行定义了线程的工作内容。每个线程会产生若干个随机数,完成工作后,记录并返回所消耗的时间。
最后是main函数,它分别对上述两种情况进行测试,并打印了测试的耗时:
public static void main(String[] args) throws InterruptedException, ExecutionException {Future<Long>[] futs = new Future[THREAD_COUNT];for(int i=0; i<THREAD_COUNT; i++) {futs[i] = exe.submit(new RndTask(0));}long totaltime = 0;for(int i=0; i<THREAD_COUNT; i++) {totaltime += futs[i].get();}System.out.println("多线程访问同一个Random实例:"+ totaltime + "ms");for(int i=0; i<THREAD_COUNT; i++) {futs[i] = exe.submit(new RndTask(1));}totaltime = 0;for(int i=0; i<THREAD_COUNT; i++) {totaltime += futs[i].get();}System.out.println("使用ThreadLocal包装Random实例:" + totaltime + "ms");exe.shutdown();
}
上述代码的运行结果,可能如下:
pool-1-thread-1 spend 2206ms
pool-1-thread-3 spend 2791ms
pool-1-thread-2 spend 2793ms
pool-1-thread-4 spend 2803ms
多线程访问同一个Random实例:10593ms
pool-1-thread-4 spend 213ms
pool-1-thread-2 spend 224ms
pool-1-thread-1 spend 225ms
pool-1-thread-3 spend 235ms
使用ThreadLocal包装Random实例:897ms
很明显,在多线程共享一个Random实例的情况下,总耗时达10秒之多(这里指4个线程的耗时总和)。而在ThreadLocal模式下,仅耗时0.8秒左右。


注:本篇博客内容摘自《 Java 高并发程序设计》

这篇关于《Java高并发程序设计》学习 --4.3 ThreadLocal的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/714925

相关文章

Java 正则表达式URL 匹配与源码全解析

《Java正则表达式URL匹配与源码全解析》在Web应用开发中,我们经常需要对URL进行格式验证,今天我们结合Java的Pattern和Matcher类,深入理解正则表达式在实际应用中... 目录1.正则表达式分解:2. 添加域名匹配 (2)3. 添加路径和查询参数匹配 (3) 4. 最终优化版本5.设计思

Java使用ANTLR4对Lua脚本语法校验详解

《Java使用ANTLR4对Lua脚本语法校验详解》ANTLR是一个强大的解析器生成器,用于读取、处理、执行或翻译结构化文本或二进制文件,下面就跟随小编一起看看Java如何使用ANTLR4对Lua脚本... 目录什么是ANTLR?第一个例子ANTLR4 的工作流程Lua脚本语法校验准备一个Lua Gramm

Java字符串操作技巧之语法、示例与应用场景分析

《Java字符串操作技巧之语法、示例与应用场景分析》在Java算法题和日常开发中,字符串处理是必备的核心技能,本文全面梳理Java中字符串的常用操作语法,结合代码示例、应用场景和避坑指南,可快速掌握字... 目录引言1. 基础操作1.1 创建字符串1.2 获取长度1.3 访问字符2. 字符串处理2.1 子字

Java Optional的使用技巧与最佳实践

《JavaOptional的使用技巧与最佳实践》在Java中,Optional是用于优雅处理null的容器类,其核心目标是显式提醒开发者处理空值场景,避免NullPointerExce... 目录一、Optional 的核心用途二、使用技巧与最佳实践三、常见误区与反模式四、替代方案与扩展五、总结在 Java

基于Java实现回调监听工具类

《基于Java实现回调监听工具类》这篇文章主要为大家详细介绍了如何基于Java实现一个回调监听工具类,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录监听接口类 Listenable实际用法打印结果首先,会用到 函数式接口 Consumer, 通过这个可以解耦回调方法,下面先写一个

使用Java将DOCX文档解析为Markdown文档的代码实现

《使用Java将DOCX文档解析为Markdown文档的代码实现》在现代文档处理中,Markdown(MD)因其简洁的语法和良好的可读性,逐渐成为开发者、技术写作者和内容创作者的首选格式,然而,许多文... 目录引言1. 工具和库介绍2. 安装依赖库3. 使用Apache POI解析DOCX文档4. 将解析

Java字符串处理全解析(String、StringBuilder与StringBuffer)

《Java字符串处理全解析(String、StringBuilder与StringBuffer)》:本文主要介绍Java字符串处理全解析(String、StringBuilder与StringBu... 目录Java字符串处理全解析:String、StringBuilder与StringBuffer一、St

springboot整合阿里云百炼DeepSeek实现sse流式打印的操作方法

《springboot整合阿里云百炼DeepSeek实现sse流式打印的操作方法》:本文主要介绍springboot整合阿里云百炼DeepSeek实现sse流式打印,本文给大家介绍的非常详细,对大... 目录1.开通阿里云百炼,获取到key2.新建SpringBoot项目3.工具类4.启动类5.测试类6.测

Spring Boot循环依赖原理、解决方案与最佳实践(全解析)

《SpringBoot循环依赖原理、解决方案与最佳实践(全解析)》循环依赖指两个或多个Bean相互直接或间接引用,形成闭环依赖关系,:本文主要介绍SpringBoot循环依赖原理、解决方案与最... 目录一、循环依赖的本质与危害1.1 什么是循环依赖?1.2 核心危害二、Spring的三级缓存机制2.1 三

在Spring Boot中浅尝内存泄漏的实战记录

《在SpringBoot中浅尝内存泄漏的实战记录》本文给大家分享在SpringBoot中浅尝内存泄漏的实战记录,结合实例代码给大家介绍的非常详细,感兴趣的朋友一起看看吧... 目录使用静态集合持有对象引用,阻止GC回收关键点:可执行代码:验证:1,运行程序(启动时添加JVM参数限制堆大小):2,访问 htt