人手一支笔:ThreadLocal(怎么优化多线程中的锁?)

简介: 人手一支笔:ThreadLocal(怎么优化多线程中的锁?)

🔎这里是多线程加油站
👍如果对你有帮助,给博主一个免费的点赞以示鼓励
欢迎各位🔎点赞👍评论收藏⭐️

人手一支笔:ThreadLocal

除了控制资源的访问外,我们还可以通过增加资源来保证所有对象的线程安全。比如,让100个人填写个人信息表,如果只有一支笔,那么大家就得挨个填写,对于管理人员来说,必须保证大家不会去哄抢这仅存的一支笔,否则,谁也填不完。从另外一个角度出发,我们可以准备100支笔,人手一支,那么所有人很快就能完成表格的填写工作。
如果说锁使用的是第一种思路,那么ThreadLocal 使用的就是第二种思路。

一、ThreadLocal的简单使用

  • 从ThreadLocal的名字上可以看到,这是一个线程的局部变量。也就是说,只有当前线程可以访问。既然是只有当前线程可以访问的数据,自然是线程安全的。
  • 下面来看一个简单的示例。
private static final SimpleDateFormat sdf = new SimpleDateFormat("yvyyy-MM-dd HH:mm:ss");
public static class ParseDate implements Runnable{
int i=0;
public ParseDate(int i) {this.i=i;}public void run() {
try{
Date t=sdf.parse("2015-03-29 19:29:"+i告60);System.out.print1n(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对象实例来解析字符串类型的日期。执行上述代码,一般来说,很可能得到一些异常(篇幅有限不再给出堆栈,只给出异常名称):

在这里插入图片描述

  • 出现这些问题的原因是,SimipleDateFormat.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;}public void run (){
try {
if(tl.get ()==null){
tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));)
Date t=tl.get ().parse("2015-03-2919:29:"+i%60);System.out.println(i+":"+t);
}catch (ParseException e){
e.printStackTrace();
}}}
  • 在上述代码第7~9行中,如果当前线程不持有SimpleDateformat对象实例,那么就新建一个并把它设置到当前线程中,如果已经持有,则直接使用。
  • 从这里也可以看到,为每一个线程分配一个对象的工作并不是由ThreadLocal来完成的,而是需要在应用层面保证的。如果在应用上为每一个线程分配了相同的对象实例,那么ThreadLocal也不能保证线程安全,这点也需要大家注意。

注意:为每一个线程分配不同的对象,需要在应用层面保证ThreadLocal 只起到了简单的容器作用。

二、ThreadLocal的实现原理

  • 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);else
createMap(t, value);
}
  • 在set时,首先获得当前线程对象,然后通过getMap()方法拿到线程的ThreadLocalMap,并将值存入ThreadLocalMap
    中。而ThreadLocalMap可以理解为一个Map(虽然不是,但是你可以把它简单地理解成HashMap),但是它是定义在Thread
    内部的成员。注意下面的定义是从 Thread类中摘出来的:

在这里插入图片描述

  • 而设置到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取得内部的实际数据。
  • 在了解了ThreadLocal的内部实现后,我们自然会引出一个问题:那就是这些变量是维护在Thread类内部的(ThreadLocalMap定义所在类),这也意味着只要线程不退出,对象的引用将一直存在。
  • 当线程退出时,Thread类会进行一些清理工作,其中就包括清理ThreadLocalMap,注意下述代码的加粗部分:
/**
*在线程退出前,由系统回调,进行资源清理*/
private void exit(){
if (group != null){
group. threadTerminated (this);group= null;
)
target = null;/*加速资源清理*/
threadLocals = null;
inheritableThreadLocals = null;
inheritedAccessControlContext = null;blocker = null;
uncaughtExceptionHandler = null;
}
  • 因此,使用线程池就意味着当前线程未必会退出(比如固定大小的线程池,线程总是存在)。如果这样,将一些大的对象设置到ThreadLocal中(它实际保存在线程持有的threadLocalsMap内),可能会使系统出现内存泄漏的可能(这里我的意思是:你设置了对象到ThreadLocal中,但是不清理它,在你使用几次后,这个对象也不再有用了,但是它却无法被回收)。
  • 此时,如果你希望及时回收对象,最好使用ThreadLocal.remove()方法将这个变量移除。就像我们习惯性地关闭数据库连接一样。如果你确实不需要这个对象了,就应该告诉虚拟机,请把它回收,防止内存泄漏。
  • 另外一种有趣的情况是JDK也可能允许你像释放普通变量一样释放ThreadLocal。比如,我们有时候为了加速垃圾回收,会特意写出类似obj=null 的代码。如果这么做,那么 obj所指向的对象就会更容易地被垃圾回收器发现,从而加速回收。
  • 同理,如果对于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 ge");
};
static volatile CountDownLatch cd = new CountDownLatch (10000);public static class ParseDate implements Runnable{
int i=0;
public ParseDate(int i){this.i =i;
public void run({
try {
if(tl.get( -=null){
tl.set (new SimpleDateFormat("yvyyy-MM-dd HH:mm:ss"){
protected 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-2919:29:"+i%60);
} catch (ParseException e){
e.printstackTrace(;
l 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!! ");
}}
  • 上述案例是为了跟踪ThreadLocal对象,以及内部SimpleDateFormat对象的垃圾回收。为此,我们在第3行代码和第17行代码中重载了finalize()方法。这样,我们在对象被回收时,就可以看到它们的踪迹。
  • 在主函数main中,先后进行了两次任务提交,每次10000个任务。在第一次任务提交后,在代码第39行,我们将tl设置为null,并进行一次GC。接着,我们进行第二次任务提交,完成后,在代码第50行再进行一次GC。
  • 执行上述代码,最有可能的一种输出如下所示。

在这里插入图片描述

  • 注意这些输出所代表的含义。首先,线程池中10个线程都各自创建了一个SimpleDateFormat对象实例。接着进行第一次GC,可以看到ThreadLocal对象被回收了(这里使用了匿名类,所以类名看起来有点怪,这个类就是第2行创建的tl对象)。提交第2次任务,这次一样也创建了10个SimpleDateFormat对象,然后进行第二次GC。在第二次GC后,第一次创建的10个SimpleDateFormat的子类实例全部被回收。虽然我们没有手工remove()这些对象,但是系统依然有可能回收它们(注意,这段代码是在JDK7中输出的,在JDK 8中,也许得不到类似的输出,大家可以比较两个JDK版本之间线程持有ThreadLocal变量的不同)。
  • 要了解这里的回收机制,我们需要更进一步了解Thread.ThreadLocalMap的实现。之前我们说过,ThreadLocalMap是一个类似HashMap 的东西。更准确地说,它更加类似WeakHashMap.
  • ThreadLocalMap的实现使用了弱引用。弱引用是比强引用弱得多的引用。Java虚拟机在垃圾回收时,如果发现弱引用,就会立即回收。ThreadLocalMap内部由一系列Entry构成,每一个Entry都是WeakReference。
static class Entry extends weakReference<ThreadLocal>{
/**The value associated with this ThreadLocal.*/0bject value;
Entry(ThreadLocal k,Object v){
super(k);
value =v;
}}
  • 这里的参数k就是Map 的 key,v就是Map的 value,其中k 也是ThreadLocal实例,作为弱引用使用(super(k)就是调用了WeakReference的构造函数)。因此,虽然这里使用ThreadLocal作为 Map的key,但是实际上,它并不真的持有ThreadLocal 的引用。而当ThreadLocal的外部强引用被回收时,ThreadLocalMap中的key就会变成null。当系统进行ThreadLocalMap清理时(比如将新的变量加入表中,就会自动进行一次清理,虽然JDK不一定会进行一次彻底的扫描,但显然在这个案例中,它奏效了),就会将这些垃圾数据回 收。ThreadLocal 的回收机制,

三、对性能有何帮助

为每一个线程分配一个独立的对象对系统性能也许是有帮助的。当然了,这也不一定,这完全取决于共享对象的内部逻辑。如果共享对象对于竞争的处理容易引起性能损失,我们还是应该考虑使用ThreadLocal为每个线程分配单独的对象。一个典型的案例就是在多线程下产生随机数。

摘自高并发多线程程序设计一书,大力推荐

相关文章
|
3月前
|
安全 Java 编译器
线程安全问题和锁
本文详细介绍了线程的状态及其转换,包括新建、就绪、等待、超时等待、阻塞和终止状态,并通过示例说明了各状态的特点。接着,文章深入探讨了线程安全问题,分析了多线程环境下变量修改引发的数据异常,并通过使用 `synchronized` 关键字和 `volatile` 解决内存可见性问题。最后,文章讲解了锁的概念,包括同步代码块、同步方法以及 `Lock` 接口,并讨论了死锁现象及其产生的原因与解决方案。
98 10
线程安全问题和锁
|
3月前
|
安全 Java 调度
Java编程时多线程操作单核服务器可以不加锁吗?
Java编程时多线程操作单核服务器可以不加锁吗?
50 2
|
5天前
|
并行计算 算法 安全
面试必问的多线程优化技巧与实战
多线程编程是现代软件开发中不可或缺的一部分,特别是在处理高并发场景和优化程序性能时。作为Java开发者,掌握多线程优化技巧不仅能够提升程序的执行效率,还能在面试中脱颖而出。本文将从多线程基础、线程与进程的区别、多线程的优势出发,深入探讨如何避免死锁与竞态条件、线程间的通信机制、线程池的使用优势、线程优化算法与数据结构的选择,以及硬件加速技术。通过多个Java示例,我们将揭示这些技术的底层原理与实现方法。
44 3
|
27天前
|
存储 监控 小程序
Java中的线程池优化实践####
本文深入探讨了Java中线程池的工作原理,分析了常见的线程池类型及其适用场景,并通过实际案例展示了如何根据应用需求进行线程池的优化配置。文章首先介绍了线程池的基本概念和核心参数,随后详细阐述了几种常见的线程池实现(如FixedThreadPool、CachedThreadPool、ScheduledThreadPool等)的特点及使用场景。接着,通过一个电商系统订单处理的实际案例,分析了线程池参数设置不当导致的性能问题,并提出了相应的优化策略。最终,总结了线程池优化的最佳实践,旨在帮助开发者更好地利用Java线程池提升应用性能和稳定性。 ####
|
3天前
|
Java 关系型数据库 MySQL
【JavaEE“多线程进阶”】——各种“锁”大总结
乐/悲观锁,轻/重量级锁,自旋锁,挂起等待锁,普通互斥锁,读写锁,公不公平锁,可不可重入锁,synchronized加锁三阶段过程,锁消除,锁粗化
|
1月前
|
供应链 安全 NoSQL
PHP 互斥锁:如何确保代码的线程安全?
在多线程和高并发环境中,确保代码段互斥执行至关重要。本文介绍了 PHP 互斥锁库 `wise-locksmith`,它提供多种锁机制(如文件锁、分布式锁等),有效解决线程安全问题,特别适用于电商平台库存管理等场景。通过 Composer 安装后,开发者可以利用该库确保在高并发下数据的一致性和安全性。
39 6
|
1月前
|
存储 监控 安全
深入理解ThreadLocal:线程局部变量的机制与应用
在Java的多线程编程中,`ThreadLocal`变量提供了一种线程安全的解决方案,允许每个线程拥有自己的变量副本,从而避免了线程间的数据竞争。本文将深入探讨`ThreadLocal`的工作原理、使用方法以及在实际开发中的应用场景。
59 2
|
2月前
|
调度 Android开发 开发者
构建高效Android应用:探究Kotlin多线程优化策略
【10月更文挑战第11天】本文探讨了如何在Kotlin中实现高效的多线程方案,特别是在Android应用开发中。通过介绍Kotlin协程的基础知识、异步数据加载的实际案例,以及合理使用不同调度器的方法,帮助开发者提升应用性能和用户体验。
68 4
|
3月前
|
存储 缓存 安全
【Java面试题汇总】多线程、JUC、锁篇(2023版)
线程和进程的区别、CAS的ABA问题、AQS、哪些地方使用了CAS、怎么保证线程安全、线程同步方式、synchronized的用法及原理、Lock、volatile、线程的六个状态、ThreadLocal、线程通信方式、创建方式、两种创建线程池的方法、线程池设置合适的线程数、线程安全的集合?ConcurrentHashMap、JUC
|
2月前
|
运维 API 计算机视觉
深度解密协程锁、信号量以及线程锁的实现原理
深度解密协程锁、信号量以及线程锁的实现原理
50 2