ThreadLocal深入剖析
JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序,ThreadLocal并不是一个Thread,而是Thread的局部变量。
线程局部变量高效地为每个使用它的线程提供单独的线程局部变量值的副本。每个线程只能看到与自己相联系的值,而不知道别的线程可能正在使用或修改它们自己的副本。
该类提供了线程局部 (thread-local)变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户ID 或事务 ID)相关联。
每个线程都保持对其线程局部变量副本的隐式引用,只要线程是活动的并且 ThreadLocal 实例是可访问的;在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。
ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单:在ThreadLocal类中定义了一个ThreadLocalMap,每一个Thread中都有一个该类型的变量——threadLocals——用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。
ThreadLocal类的四个方法如下:
T get()
返回此线程局部变量的当前线程副本中的值。如果变量没有用于当前线程的值,则先将其初始化为调用 initialValue() 方法返回的值。
protected T initialValue()
返回此线程局部变量的当前线程的“初始值”。这个方法是一个延迟调用方法,线程第一次使用 get() 方法访问变量时将调用此方法,但如果线程之前调用了 set(T) 方法,则不会对该线程再调用 initialValue 方法。通常,此方法对每个线程最多调用一次,但如果在调用 get() 后又调用了 remove(),则可能再次调用此方法。该实现返回 null;如果程序员希望线程局部变量具有 null以外的值,则必须为 ThreadLocal 创建子类,并重写此方法。通常将使用匿名内部类完成此操作。
void remove()
移除此线程局部变量当前线程的值。如果此线程局部变量随后被当前线程读取,且这期间当前线程没有设置其值,则将调用其 initialValue() 方法重新初始化其值。这将导致在当前线程多次调用 initialValue 方法。
void set(Tvalue)
将此线程局部变量的当前线程副本中的值设置为指定值。大部分子类不需要重写此方法,它们只依靠 initialValue() 方法来设置线程局部变量的值。
先看下get方法的实现:
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(); }第一行代码获取当前线程。
第二行代码利用getMap()方法获取当前线程对应的ThreadLocalMap。
getMap()方法的代码如下:
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }可见,getMap()方法返回的是Thread类中一个叫“threadLocals”的字段。查看Thread类的源码,可以发现,每个Thread实例都有一个ThreadLocalMap类型的成员变量:
ThreadLocal.ThreadLocalMap threadLocals =null;ThreadLocalMap实际上是ThreadLocal类中的一个静态内部类:
static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal> { Object value; Entry(ThreadLocal k, Object v) { super(k); value = v; } }ThreadLocalMap的Entry继承了WeakReference,并且使用ThreadLocal作为键值。正因如此,第四行代码:
ThreadLocalMap.Entrye = map.getEntry(this);
获取Entry时传入的参数是this,即当前的ThreadLocal实例,而非第一行代码获取的当前线程t。
如果得到的Entry不为空,直接返回,如果为空,则调用setInitialValue()方法:
private T setInitialValue() { T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; }如果当前线程的threadLocals不为空,将初始化值放入threadLocals,如果为空,则新建一个ThreadLocalMap,赋值给当前线程的threadLocals。
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }看到到这里可能有点乱,从头理一下思路。首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal实例,value为变量副本(即T类型的变量)。
初始时,在Thread里面的threadLocals为null,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。
在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。
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); }与get()类似,也是先获取当前线程的ThreadLocalMap,然后以当前ThreadLocal、指定value为键值对的Entry存入该ThreadLocalMap(如果ThreadLocalMap为空,则需要先创建再存入)。
initialValue()是一个protected方法,默认直接返回null,一般需要重写的,用以设置初始值。
protected T initialValue() { return null; }remove()用来移除当前线程中变量的副本,实现起来很简单,直接删除以当前ThreadLocal为键值的Entry:
public void remove() { ThreadLocalMap m =getMap(Thread.currentThread()); if (m != null) m.remove(this); }下面通过代码来体会ThreadLocal的魅力:
public class Test { //该类的第一个ThreadLocal变量,用来存储线程ID ThreadLocal<Long> longLocal = new ThreadLocal<Long>(){ protected Long initialValue() { //覆写initialValue()方法 return Thread.currentThread().getId(); }; }; //该类的第二个ThreadLocal变量,用来存储线程名称 ThreadLocal<String> stringLocal = new ThreadLocal<String>(){; protected String initialValue() { //覆写initialValue()方法 return Thread.currentThread().getName(); }; }; public void set() { longLocal.set(Thread.currentThread().getId()); stringLocal.set(Thread.currentThread().getName()); } public long getLong() { return longLocal.get(); } public String getString() { return stringLocal.get(); } public static void main(String[] args)throws InterruptedException { final Test test = new Test(); Thread thread1 = new Thread(){ public void run() { test.set(); System.out.println(test.getLong()); System.out.println(test.getString()); }; }; Thread thread2 = new Thread(){ public void run() { test.set(); System.out.println(test.getLong()); System.out.println(test.getString()); }; }; thread1.start(); thread1.join(); thread2.start(); thread2.join(); System.out.println(test.getLong()); System.out.println(test.getString()); } }打印结果如下:
8
Thread-0
9
Thread-1
1
main
我们来分析一下上面的代码,需要注意的是,实际上一共运行了三个线程:线程thread1、线程thread2和主线程main。
虽然三个线程执行了同样的代码
System.out.println(test.getLong());
System.out.println(test.getString());
但三次打印的结果并不一样。
首先启动thread1,执行test.set()的时候,是将thread1的线程ID与名称存入了thread1的threadLocals变量中,然后打印出存入的线程ID与名称。
然后启动thread1,执行test.set()的时候,是将thread2的线程ID与名称存入了thread2的threadLocals变量中,与thread1实例中存入的变量是互不影响的两个副本。
同样的,最后打印的是主线程main的线程ID与名称,与thread1和thread2当中存入的变量副本也是互不影响。
由此证明,通过ThreadLocal达到了在每个线程中创建变量副本的效果。
在ThreadLocal源码中我们经常看到这样的语句:
Thread t =Thread.currentThread(); ThreadLocalMap map = getMap(t);其精髓就在于ThreadLocal的set()方法、get()等方法都是通过Thread.currentThread()这把钥匙打开了当前线程的“小金库”,然后针对每个线程进行互不影响的存取操作。
线程局部变量常被用来描绘有状态“单例”(Singleton) 或线程安全的共享对象,或者是通过把不安全的整个变量封装进 ThreadLocal,或者是通过把对象的特定于线程的状态封装进 ThreadLocal。例如,在与数据库有紧密联系的应用程序中,程序的很多方法可能都需要访问数据库。在系统的每个方法中都包含一个 Connection 作为参数是不方便的。用“单例”来访问连接可能是一个虽然更粗糙,但却方便得多的技术。然而,多个线程不能安全地共享一个 JDBC Connection。通过使用“单例”中的 ThreadLocal,我们就能让我们的程序中的任何类容易地获取每线程 Connection 的一个引用。当要给线程初始化一个特殊值时,需要自己实现ThreadLocal的子类并重写initialValue()方法(该方法缺省地返回null),通常使用一个内部匿名类对ThreadLocal进行子类化。
如下例所示:
public class ConnectionDispenser { private static class ThreadLocalConnection extends ThreadLocal { public Object initialValue() { return DriverManager.getConnection(ConfigurationSingleton.getDbUrl()); } } private ThreadLocalConnection conn = new ThreadLocalConnection(); public static Connection getConnection(){ return (Connection) conn.get(); } }这是非常常用的一种方案。EasyDBO中创建jdbc连接上下文就是这样做的:
public class JDBCContext{ private static Logger logger =Logger.getLogger(JDBCContext.class); private DataSource ds; protected Connection connection; public static JDBCContext getJdbcContext(javax.sql.DataSource ds){ if(jdbcContext==null) jdbcContext =new JDBCContextThreadLocal(ds); JDBCContext context = (JDBCContext)jdbcContext.get(); if (context == null) context = new JDBCContext(ds); return context; } // 继承了ThreadLocal的内部类 private static class JDBCContextThreadLocal extends ThreadLocal { public javax.sql.DataSource ds; public JDBCContextThreadLocal(javax.sql.DataSource ds){ this.ds=ds; } //重写initialValue()方法 protected synchronized Object initialValue() { return new JDBCContext(ds); } } }ThreadLocal的思想在Hibernate、Spring框架中广为使用。
下面的实例能够体现Spring对有状态Bean的改造思路:
TopicDao:非线程安全
public class TopicDao { private Connection conn;//一个非线程安全的变量 public void addTopic(){ Statement stat =conn.createStatement();//引用非线程安全变量 … } }由于①处的conn是成员变量,因为addTopic()方法是非线程安全的,必须在使用时创建一个新TopicDao实例(非singleton)。下面使用ThreadLocal对conn这个非线程安全的“状态”进行改造:
TopicDao:线程安全
public class TopicDao { //使用ThreadLocal保存Connection变量 private staticThreadLocal<Connection> connThreadLocal = new ThreadLocal<Connection>(); public static Connection getConnection(){ //如果connThreadLocal没有本线程对应的Connection创建一个新的Connection,并将其保存到线程本地变量中。 } public void addTopic() { //从ThreadLocal中获取线程对应的Connection Statement stat =getConnection().createStatement(); } }不同的线程在使用TopicDao时,这样,就保证了不同的线程使用线程相关的Connection,而不会使用其它线程的Connection。因此,这个TopicDao就可以做到singleton共享了。
当然,这个例子本身很粗糙,将Connection的ThreadLocal直接放在DAO只能做到本DAO的多个方法共享Connection时不发生线程安全问题,但无法和其它DAO共用同一个Connection,要做到同一事务多DAO共享同一Connection,必须在一个共同的外部类使用ThreadLocal保存Connection。
ThreadLocal和线程同步机制相比有什么优势呢?ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。
在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。
而ThreadLocal则从另一个角度来解决多线程的并发访问。在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。
由于ThreadLocal中可以持有任何类型的对象,低版本JDK所提供的get()返回的是Object对象,需要强制类型转换。但JDK 5.0通过泛型很好的解决了这个问题,在一定程度地简化ThreadLocal的使用。
概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。
ThreadLocal深入剖析的更多相关文章
- ThreadLocal及InheritableThreadLocal的原理剖析
我们知道,线程的不安全问题,主要是由于多线程并发读取一个变量而引起的,那么有没有一种办法可以让一个变量是线程独有的呢,这样不就可以解决线程安全问题了么.其实JDK已经为我们提供了ThreadLocal ...
- 对ThreadLocal实现原理的一点思考
前言 在<透彻理解Spring事务设计思想之手写实现>中,已经向大家揭示了Spring就是利用ThreadLocal来实现一个线程中的Connection是同一个,从而保证了事务.本篇博客 ...
- 图学java基础篇之并发
概述 并发处理本身就是编程开发重点之一,同时内容也很繁杂,从底层指令处理到上层应用开发都要涉及,也是最容易出问题的地方.这块知识也是评价一个开发人员水平的重要指标,本人自认为现在也只是学其皮毛,因此本 ...
- Java多线程9:ThreadLocal源码剖析
ThreadLocal源码剖析 ThreadLocal其实比较简单,因为类里就三个public方法:set(T value).get().remove().先剖析源码清楚地知道ThreadLocal是 ...
- 深入剖析ThreadLocal
Java并发编程:深入剖析ThreadLocal 想必很多朋友对ThreadLocal并不陌生,今天我们就来一起探讨下 ThreadLocal的使用方法和实现原理.首先,本文先谈一下对ThreadLo ...
- Java并发编程:深入剖析ThreadLocal(转载)
Java并发编程:深入剖析ThreadLocal(转载) 原文链接:Java并发编程:深入剖析ThreadLocal 想必很多朋友对ThreadLocal并不陌生,今天我们就来一起探讨下ThreadL ...
- (转)Java并发编程:深入剖析ThreadLocal
Java并发编程:深入剖析ThreadLoca Java并发编程:深入剖析ThreadLocal 说下自己的理解:使用ThreadLocal能够实现空间换时间,重在理解ThreadLocal是如何复制 ...
- ThreadLocal终极源码剖析
目录一.ThreadLocal1.1 源码注释1.2 源码剖析 散列算法-魔数0x61c88647 set操作 get操作 remove操作1.3 功能测试1.4 应用 ...
- ThreadLocal终极源码剖析-一篇足矣!
本文较深入的分析了ThreadLocal和InheritableThreadLocal,从4个方向去分析:源码注释.源码剖析.功能测试.应用场景. 一.ThreadLocal 我们使用ThreadLo ...
随机推荐
- tagName与nodeName的区别
首先介绍DOM里常见的三种节点类型(总共有12种,如docment):元素节点,属性节点以及文本节点,例如<h2 class="title">head</h2&g ...
- Ubuntu16.04开机引导缺失Win10
Ubuntu正常开机的情况下: sudo update-grub # 如果grub丢失, 就先sudo apt install grub Ubuntu不能正常开下: 进入Ubuntu引导, 不要正常进 ...
- Spring Cloud学习笔记-008
继承特性 通过上节的示例实践,当使用Spring MVC的注解来绑定服务接口时,几乎完全可以从服务提供方的Controller中依靠复制操作,构建出相应的服务客户端绑定接口.既然存在这么多复制操作,自 ...
- 基于angularJS搭建的管理系统
前言 angularJS搭建的系统,是一年前用的技术栈,有些地方比较过时,这里只是介绍实现思路 前端架构 工程目录 项目浅析 项目依赖包配置package.json { "name" ...
- jQuery系列 第五章 jQuery框架动画特效
第五章 jQuery框架动画特效 5.1 jQuery动画特效说明 jQuery框架中为我们封装了众多的动画和特效方法,只需要调用对应的动画方法传递合适的参数,就能够方便的实现一些炫酷的效果,而且jQ ...
- 同步IO,异步IO,阻塞IO,非阻塞IO
同步(synchronous):一个进程在执行某个任务时,另外一个进程必须等待其执行完毕,才能继续执行 #所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不会返回.按照这个定义, 其实 ...
- Python默认版本切换
Mac上自带python2.7 版本,但是我又下了一个3.7版本(下载的版本默认安装在 /Library/Frameworks/Python.framework/Versions/3.7/bin/py ...
- Mysql之基本操作与数据类型
进入mysql: mysql -hlocalhost -uroot -p; mysql -uroot -p密码; 查看帮助文档: help 查看名 database(s); 创建数据库: create ...
- [USACO 12DEC]Running Away From the Barn
Description It's milking time at Farmer John's farm, but the cows have all run away! Farmer John nee ...
- [JLOI 2011]飞行路线&[USACO 09FEB]Revamping Trails
Description Alice和Bob现在要乘飞机旅行,他们选择了一家相对便宜的航空公司.该航空公司一共在n个城市设有业务,设这些城市分别标记为0到n-1,一共有m种航线,每种航线连接两个城市,并 ...