ThreadLocal是什么?
早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思。
对比Synchronized,我们就能更好的理解——Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离
ThreadLocal的用法(存在意义)
一、同一线程内共享数据
一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程,如图所示:
同一线程贯通三层这样你就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有关联的对象引用到的都是同一个变量。
例如下面这个例子:
我需要在执行完毕的时候对事物提交,发生异常的时候对事务进行回滚,那么就要求我们进行提交/回滚的Connection连接和执行增删查改的连接是同一个——即一个事务一个连接,问题在于,我们获取连接的方式如果是通过连接池(DataSource)来完成,那么每次获取的连接都不一定是相同的!
@Before
public void before(){
ad = new AccountDaoImp();
}
@Test
public void transfer(){
Connection conn = DataSourceUtils.getConnection();
try {
conn.setAutoCommit(false);
ad.decreaMoney(2, 2000);
int a = 1/0;
ad.increaMoney(1,2000);
conn.commit();
} catch (SQLException e) {
try {
conn.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
}
}
这个时候我们就可以借助ThreadLocal将连接嵌在这个线程中, 因为即使是数据库和业务两层不相干的情况下,整个项目的执行还是依赖于同一条线程的——主线程,那么我们将事务开始时就直接将这个连接钉死在这个主线程上,那么之后回滚/提交用的连接都会是同一个了!
public class DataSourceUtils {
private static DataSource ds = null;
private static ThreadLocal<Connection> tl = new ThreadLocal<>();
static {
ds = new ComboPooledDataSource();
}
public static DataSource getDataSource(){
return ds;
}
public static Connection getConnection(){
Connection conn = tl.get();//后面获取连接都是获取到同线程对应的同一个connection
if(conn == null) {
try {
tl.set(ds.getConnection());//首次将连接借tl置入到当前线程的ThreadLocalMap中
conn = tl.get();
} catch (SQLException e) {
e.printStackTrace();
}
}
return conn;
}
}
二、不同线程实现数据隔离
public class TestNum {
// ①通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值
private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>() {
public Integer initialValue() {
return 0;
}
};
// ②获取下一个序列值
public int getNextNum() {
seqNum.set(seqNum.get() + 1);
return seqNum.get();
}
public static void main(String[] args) {
TestNum sn = new TestNum();
// ③ 3个线程共享sn,各自产生序列号
TestClient t1 = new TestClient(sn);
TestClient t2 = new TestClient(sn);
TestClient t3 = new TestClient(sn);
t1.start();
t2.start();
t3.start();
}
private static class TestClient extends Thread {
private TestNum sn;
public TestClient(TestNum sn) {
this.sn = sn;
}
public void run() {
for (int i = 0; i < 3; i++) {
// ④每个线程打出3个序列值
System.out.println("thread[" + Thread.currentThread().getName() + "] --> sn["
+ sn.getNextNum() + "]");
}
}
}
}
通常我们通过匿名内部类的方式定义ThreadLocal的子类,提供初始的变量值,如例子中①处所示。TestClient线程产生一组序列号,在③处,我们生成3个TestClient,它们共享同一个TestNum实例。运行以上代码,在控制台上输出以下的结果:
thread[Thread-0] --> sn[1]
thread[Thread-1] --> sn[1]
thread[Thread-2] --> sn[1]
thread[Thread-1] --> sn[2]
thread[Thread-0] --> sn[2]
thread[Thread-1] --> sn[3]
thread[Thread-2] --> sn[2]
thread[Thread-0]—>sn[3]
thread[Thread-2] --> sn[3]
注意:我们使用ThreadLocal为不同线程分配副本,副本并不是指同一个对象,因此倘若为不同的线程set()同一个对象的引用,还是无法避免导致不同线程都对该对象进行了操作,因此我们分配的副本就是不同的对象。ThreadLocal的最大意义就在于它实现了将副本写进了Thread对象内部的Threadlocals 这个映射对象(它是一个ThreadLocal.ThreadLocalMap类对象),我们的set、get方法都只能从当前线程中设置或者获得副本。
ThreadLocal实现机制
那么到底ThreadLocal类是如何实现这种“为每个线程提供不同的变量拷贝”的呢?先来看一下ThreadLocal的set()方法的源码是如何实现的:
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap是ThreadLocal类的一个静态内部类,它实现了键值对的设置和获取(对比Map对象来理解),每个线程中都有一个独立的ThreadLocalMap副本,它所存储的值,只能被当前线程读取和修改。
- 获取此时当前线程对象
- 尝试获取该线程内部的ThreadLocalMap对象
- 若为空(线程内ThreadLocals默认是null),创建一个map给这个线程t
否则不为空的情况说明该线程内已经有了对应的键值对(this–xxx),而此时做的就是替换xxx
我们接着看上面代码中出现的getMap和createMap方法的实现:
/**
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
* @param t the current thread
* @param firstValue value for the initial entry of the map
* @param map the map to store.
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
接下来再看一下ThreadLocal类中的get()方法:
/**
*
* @return the current thread's value of this thread-local
*/
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对象,若这个Map是空的那么我们就设定初始值并返回,否则返回这个Map查询的结果。