另一种线程安全机制:在事务管理中起到巨大作用的 ThreadLocal
ThreadLocal 是什么
ThreadLocal 不是一个线程,而是保存线程本地化对象的容器。
当运行于多线程环境的某个对象使用 ThreadLocal 维护变量时,ThreadLocal 为每个使用该变量的线程分配一个独立的变量副本。
所以每个线程可以独立地改变自己的副本,而不会影响其他线程所对应的变量副本。
ThreadLocal 的接口方法
void set(Object value)
:设置当前线程的线程局部变量值。public Object get()
:返回当前线程所对应的线程局部变量。public void remove()
:将当前线程的局部变量值删除。目的是为了减少内存的占用。protected Object initialValue()
:返回该线程局部变量的初始值。该方法是一个protected 的方法,显然是为了让之类覆盖而设计的。这个方法是延迟调用方法,在线程第一次调用 get() 或者 set(Object) 时才执行,并且仅执行一次。
ThreadLocal 维护一份独立变量副本的思很简单:在 ThreadLocal 类中有一个 Map,用于存储每个线程的变量副本,Map 中元素的键为线程对象,值为对应线程的变量副本。
ThreadLocal 实例
public class SequenceNumber {
// 通过匿名内部类覆盖 initialValue() 方法,指定初始值
private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>() {
@Override
public Integer initialValue() {
return 0;
}
};
public int getNextNum() {
seqNum.set(seqNum.get() + 1); // 获得下一个序列值
return seqNum.get();
}
public static void main(String[] args) {
SequenceNumber sn = new SequenceNumber();
TestClient t1 = new TestClient(sn); // 3个线程共享sn,各自产生序列号
TestClient t2 = new TestClient(sn);
TestClient t3 = new TestClient(sn);
t1.start();
t2.start();
t3.start();
}
private static class TestClient extends Thread {
private SequenceNumber sn;
public TestClient(SequenceNumber sn){
this.sn = sn;
}
@Override
public void run(){
for (int i = 0; i < 3; i++) { //每个线程打印3个序列号
System.out.println("thread[" + Thread.currentThread().getName() +
"] sn[" + sn.getNextNum() + "]");
}
}
}
}
运行结果:
可以发现每个线程所产生的序列号都共享于同一个实例 SequenceNumber,但它们并没有互相干扰,而是各自产生独立的序列号。
与 Thread 同步机制比较
在同步机制中,通过对象的锁机制保证同一个时间只有一个线程访问变量。
而 ThreadLocal 是从另一个角度来解决并发问题,ThreadLocal 为没一个线程提供独立的变量副本,从而隔离了多个线程对访问数据冲突。
简单来说,Thread 机制是采用“以时间换空间” 的方式:访问串行化,对象共享化;而 ThreadLocal 采用了 “以空间换时间” 的方式:访问并行化,对象独享化。
Spring 使用 ThreadLocal 解决线程安全问题
在一般情况下,只有无状态的 Bean 才可以在多线程环境下共享。在 Spring 中,绝大部分 Bean 都可以生命为 singleton 作用域。
正是因为 Spring 对一些 Bean 中非线程安全的 ”状态性对象“ 采用 ThreadLocal 进行封装,让他们也成为线程安全的 “状态性对象”,因此,有状态的 Bean 就能过以 singleton 的方式在多线程中正常工作。
举个获取数据库连接的例子:
非线程安全
public class TopicDao {
// 1.一个非线程安全变量
private Connetion conn;
public void addTopic() {
// 2.引用非线程安全变量
Statement stat = getConnection().createStatement();
}
}
使用 ThreadLocal 进行改进,变成线程安全的状态
public class TopicDao {
// 1. 使用 ThreadLocal 保存 Connection 变量
private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<>();
public static Connection getConnection() {
// 2. 如果 connThreadLocal没有,则创建一个新的 Connection,并保存到线程本地变量中
if (connThreadLocal.get() == null) {
Connection conn = ConnectionManager.getConnection();
connThreadLocal.set(conn);
return conn;
}else {
// 3. 直接返回线程本地变量
return connThreadLocal.get();
}
}
public void addTopic() {
// 4. 从 ThreadLocal 中获取线程对应的 Connection
Statement stat = getConnection().createStatement();
}
}
这个例子本身很粗糙,将 Connection 的 ThreadLocal 直接放在 DAO 中只能做到本 DAO 的多个方法共享 Connection 时不发生线程安全问题,而无法和其他 DAO 共用一个Connection 。要做到同一个事务多DAO 共享同一个 Connection ,必须在共同的外部类使用 ThreadLocal 保存 Connection 。
但这个例子也基本上说明了 Spring 对所有状态类线程安全化的解决思路。