Java-ThreadLocal

Java-ThreadLocal

1. 认识ThreadLocal

ThreadLocal 是 Java 中一种很重要的机制 / 数据处理方式,尤其在并发中,数据是否共有、唯一,会直接影响项目的运行逻辑。一般情况下,防止并发冲突或数据不安全的做法是给对象、方法加同步锁 synchronized。但同步锁并不是万能的,例如同步锁会降低批量处理的效率,或者当业务需要保证数据的隔离性,使用同步锁则需要在方法内频繁销毁、重建对象,如果数据使用独立的处理模块,还会破坏模块化,提高耦合。为此,JDK 1.2 增加了一个工具类:即 ThreadLocal


2. 不使用ThreadLocal的问题

为了更好地理解 ThreadLocal 的设计理念,首先考虑以下两个更普遍一些的场景:

  1. 一个客户端,需要并发地和服务器交互,并且每个连接都需要持久化(需要保存 Cookie)。
  2. 一个客户端,需要并发地存取数据库,并且每个连接都可能会提交超过一个操作。

(1)如果按最简单的方式来做,每个线程都维护一套自己的网络请求框架,确实不会导致什么异常,但是第一:实际项目中不可能采用这个方案,第二:这么做简直就是“高耦合低内聚”的代表,第三:重新参考以上两条。项目中,绝大部分情况下,一个连接会话会在一个独立的线程内执行,这个线程需要维护一个仅对自己可见的 Cookie,不仅对其他会话不可见,同时也要确保只能获取到自己的 Cookie。

(2)同样,每个线程都独立维护一套数据库会话管理是不现实的,通常会封装到一个工具类中,从工具类中获取、开启、关闭会话以及提交事务等。假如有一个业务:当修改用户信息时,把这个操作记录保存下来。假如每一次的操作都从连接池获取 Connection,就有可能一个操作执行了另一个可能因为某些原因没有执行,所以一般通过以下方式来获取管理 Connection:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class DBUtil {
// 省略一些成员变量
private static Connection connection;

// 获取连接实例
public static Connection getConnection() {
try {
Class.forName(driver);
connnection = DriverManager.getConnection(url, username, password);
} catch (Exception e) {
e.printStackTrace();
}
return connnection;
}

// 关闭连接
public static void closeConnection() {
try {
if (connnection != null) {
connnection.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

然后用 Transaction 统一提交事务。到目前为止,如果是单线程做这个操作,那是没问题的,但是如果出现并发的情况呢?如果并发很低并且操作轻量,给这个业务方法上同步锁,也是没有问题的,但是如果并发稍微高一些,就不可能放个同步锁了,这时如果还使用这个方案,很有可能会出现 No operations allowed after connection closed 错误,这是因为连接是共享的,如果后启动的线程 2 先执行完并且关闭了连接,先启动的线程 1 再执行相关操作时连接已经被关闭了。


3. ThreadLocal如何解决问题

ThreadLocal 为每个线程分配了一个独立的资源副本,并在内部通过一个 Table 表来维护每个线程和其拥有的独立资源副本的映射关系,所有的线程共享这个 Table。简单点说,ThreadLocal 中通过 set() 方法存入对象,通过 get() 方法取出对象,且线程存入的对象只有该线程自己可以获取到,,每个线程也只能获取到自己之前存入的对象,如果没有存入则调用 get() 返回的是 null

因此针对以上两个场景,用 ThreadLocal 就可以很好地解决痛点。

(1)首先是会话连接的持久化,每个线程在建立连接后,调用 set()将自己的 Cookie 存入,并在需要的时候调用 get() 获取即可,对于每个会话线程,get() 到的都只是自己的 Cookie。

(2)第二个数据库连接管理,也可以把共用的 Connection 放进 ThreadLocal 中管理,改成如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class DBUtil {
// 省略一些成员变量
private static ThreadLocal<Connection> localConnection = new ThreadLocal<>();

// 获取连接实例
public static Connection getConnection() {
Connection connnection = localConnection.get();
try {
if(connection == null) {
Class.forName(driver);
connnection = DriverManager.getConnection(url, username, password);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
localConnection.set(connection);
}
return connnection;
}

// 关闭连接
public static void closeConnection() {
Connection connnection = localConnection.get();
try {
if (connnection != null) {
connnection.close();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
localConnection.remove();
}
}
}

当一个新线程调用 DBUtil.getConnection() 时,会先判断当前线程是否已经存入了一个连接,如果已经存入则直接获取并返回,否则创建一个新的连接,关闭连接时同理。这样,线程之间的连接都是自己的独立对象,不会互相影响。


4. ThreadLocal和同步锁的比较

当然,ThreadLocal 并不是万能的,相比较同步锁方式,由于每个线程都拥有自己的资源副本,因此消耗的内存也更多,需要根据具体的业务确定方案。详细分析将在之后重新整理一份独立文章。


5. ThreadLocal源码分析

暂未完成


参考文献