Java 中的 ThreadLocal

wiki

提供了线程的局部变量,每个线程各自可以通过 set/get 来对自己的这个局部变量进行操作,实现了线程的数据隔离

1. 内部设计

每个 Thread 维护一个 ThreadLocalMap,它的 key 是 ThreadLocal 本身(其实是 Entry,是它的一个弱引用),value 是真正要存储的 Object。
每个线程往某个 ThreadLocal 里存值的时候,都存入自己的 ThreadLocalMap 里,读取也是以某个 ThreadLocal 作为引用,在自己的 map 里找对应的 key,从而实现了线程隔离。

2. 一些问题

1. ThreadLocal 做为key,而不是 Thread 做为 key 的优点

  • 一个线程是可以拥有多个私有变量,key 如果是当前线程的话,还需要唯一标识 value
  • 所有线程都去操作同一个 Map,其体积有可能会膨胀,导致访问性能的下降
  • 当 Thread 销毁的时候,ThreadLocal 也会随之销毁,减少内存开销

2. 内存泄露

ThreadLocal 引用链如下:

  • ThreadLocalRef && ThreadLocalMap -> Entry key -> ThreadLocal
  • ThreadRef -> Thread -> ThreadLoalMap -> Entry value -> Object
    其内存泄露,指使用完 ThreadLocal, 其引用被回收,由于 ThreadLocalMap 只持有 ThreadLocal 的弱引用,所以 ThreadLocal 可以被 gc 回收,此时 Entry 中的 key=null,如果没有手动删除这个 Entry,以及 Thread 依然在运行的前提下,value 不会被回收,而这块 value 永远不会被访问到了,导致内存泄漏

内存泄漏需要的条件:

  • ThreadLocal 被外部回收
  • 线程被复用
  • 未调用 set/get/remove

3. 实战

常用方法:

  • get() : 用来获取ThreadLocal在当前线程中保存的变量副本
  • set(value) : 用来设置当前线程中变量的副本
  • remove() : 移除当前线程中变量的副本
  • initialValue() : protected 方法,自定义初始化方法,
1
2
3
4
5
6
7
8
9
10
// 存储相应类型数据
private static ThreadLocal<Integer> local = new ThreadLocal<>();

// 自定义初始化存储变量
private static final ThreadLocal<Integer> THREAD_LOCAL_NUM = new ThreadLocal<>() {
@Override
protected Integer initialValue() {
return 0;
}
};

常用在数据库连接的处理中

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
// 数据库连接,此处是为了针对会连接多个数据库,所以会有多个工厂配置
public static ThreadLocal<SqlSessionFactory> sessionLocal = new ThreadLocal<SqlSessionFactory>(){
@Override
protected SqlSessionFactory initialValue() {
return buildDefaultFactroy();
}
};

public static SqlSession getSession() throws Exception {
return sessionLocal.get().openSession(true);
}

public static SqlSessionFactory getSessionFactory() throws Exception {
// sessionLocal.get(Thread.currentThread())
return sessionLocal.get();
}

// 数据库配置创建和获取
public static ThreadLocal<String> dbKeyLocal = new ThreadLocal<String>();

public static void setDBKey(String dbKey) {
dbKeyLocal.set(dbKey);
}

public static String getDBKey() {
return dbKeyLocal.get();
}

public static SqlSessionFactory buildDefaultFactroy() {
SqlSessionFactory factory = null;
try {
// 默认数据源
DataSource dataSource = getDataSource();
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("development", transactionFactory, dataSource);
Configuration configuration = new Configuration(environment);
configuration.addMapper(serverMapper.class);
...
configuration.setJdbcTypeForNull(JdbcType.NULL);
factory = new SqlSessionFactoryBuilder().build(configuration);
} catch (Exception e) {
Logger logger = LoggerUtil.getLoggerByName(dbLogPath);
logger.error("创建Mybatis连接失败", e);
throw new StreamException("创建Mybatis连接失败", e);
}

return factory;
}

// 根据配置信息获取数据源(使用Druid数据源)
public static DruidDataSource getDataSource() throws Exception{
// 获取数据库配置信息
String masterCfgPath = "...";
ParameterTool dbParam = ParameterTool.fromPropertiesFile(masterCfgPath);
String dbURL = dbParam.get("jdbc.url"); // 数据库连接URL
String userName = dbParam.get("jdbc.uid"); // 数据库用户名
String password = dbParam.get("jdbc.pwd"); // 数据库密码
int initSize = dbParam.getInt("initialSize"); // 数据源初始化连接数
int minIdle = dbParam.getInt("minIdle"); // 数据源最小空闲
int maxActive = dbParam.getInt("maxActive"); // 数据源最大连接数
long maxWait = dbParam.getLong("maxWait"); // 获取连接最大等待空闲
long timeBetween = dbParam.getLong("timeBetweenEvictionRunsMillis"); // 检测间隔时长
long minTime = dbParam.getLong("minEvictableIdleTimeMillis"); // 数据源最小生存时间
boolean testIdle = dbParam.getBoolean("testWhileIdle");
boolean testBorrow = dbParam.getBoolean("testOnBorrow");
boolean testReturn = dbParam.getBoolean("testOnReturn");
String validateQuery = dbParam.get("validationQuery");
String fliters = dbParam.get("filters");
boolean pool = dbParam.getBoolean("poolPreparedStatements");
int maxPool = dbParam.getInt("maxPoolPreparedStatementPerConnectionSize");

DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(dbURL);
dataSource.setUsername(userName);
dataSource.setPassword(password);
dataSource.setInitialSize(initSize);
dataSource.setMinIdle(minIdle);
dataSource.setMaxActive(maxActive);
dataSource.setMaxWait(maxWait);
dataSource.setTimeBetweenEvictionRunsMillis(timeBetween);
dataSource.setMinEvictableIdleTimeMillis(minTime);
dataSource.setTestWhileIdle(testIdle);
dataSource.setTestOnBorrow(testBorrow);
dataSource.setTestOnReturn(testReturn);
dataSource.setValidationQuery(validateQuery);
dataSource.setFilters(fliters);
dataSource.setPoolPreparedStatements(pool);
dataSource.setMaxPoolPreparedStatementPerConnectionSize(maxPool);

dataSource.init();
return dataSource;
}