文章目录
在多线程并发环境中,要保证线程并发安全总体有2类方法。
-
使用锁,在访问资源时是互斥的、原子性的。
- 这个锁可以是Synchronized方法 或 Lock。
- 或者CAS方式的乐观锁。
-
使用线程隔离的方法。
-
变量在线程内部,在实际运行过程中只有这个线程可以读取。
-
变量在线程外部定义,但是每个线程只能操作属于该线程的变量副本。即 ThreadLocal类型的变量。
-
1. ThreadLocal的使用示例
定义了两个ThreadLocal类型的变量,线程t1设置的值,对于线程t2是不可见的。
public class ThreadLocalTest1 {
private static ThreadLocal<String> thl1 = new ThreadLocal<>();
private static ThreadLocal<Integer> thl2 = new ThreadLocal<>();
public static void main(String[] args) {
Thread t1 = new Thread(()->{
thl1.set("abc");
thl2.set(12);
thl1.remove();
thl2.remove();
System.out.println(thl1.get()+","+ thl2.get());
});
Thread t2 = new Thread(()->{
System.out.println(thl1.get()+","+ thl2.get());
});
t1.start();
t2.start();
}
}
运行结果:
2. ThreadLocal的使用场景
2.1 线程隔离的数据库连接与事务
定义一个数据库连接,每个线程拿到的数据库连接都是本线程对应的数据库连接。
public class ConnectionManager {
private static final ThreadLocal<Connection> dbConnectionLocal = new ThreadLocal<Connection>() {
@Override
//初始化数据。
//延迟调用方法,在线程第一次调用get或set时才执行,并且只执行1次。默认返回null。
protected Connection initialValue() {
try {
return DriverManager.getConnection();
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
};
public Connection getConnection() {
return dbConnectionLocal.get();
}
}
2.2 线程隔离的session会话
在每个线程session会话都是本线程对应的session。
private static final ThreadLocal threadSession = new ThreadLocal();
public static Session getSession() {
Session s = (Session) threadSession.get();
try {
if (s == null) {
s = getSessionFactory().openSession();
threadSession.set(s);
}
} catch (Exception ex) {
}
return s;
}
3. ThreadLocal原理
-
Thread类内部定义了一个ThreadLocalMap 对象
ThreadLocal.ThreadLocalMap threadLocals = null;
当线程调用threadlocal对象的set(Object o)和get方法时,实际上是维护ThreadLocalMap
对象。- 如果这个对象尚未初始化,则初始化
- set时,以threadlocal对象为key,o 为value。
- get时,以threadlocal对象为key取出value。
-
举例说明:
thl1.set("abc");thl2.set(12);
两条命令会在线程内的ThreadLocalMap对象上插入两条记录,这两条记录的key分别是thl1,thl2。值分别是"abc",12。thl1.get(); thl2.get();
命令执行时,实际上会在当前线程内的ThreadLocalMap对象上,查找key为thl1、thl2的值。
-
也就是说,每个线程内部其实都有一个专属的“记录”——ThreadLocalMap,当写入和读取时,都会以threadlocal对象的引用为key,去存储读取。
4. ThreadLocal与内存泄露
内存泄露是指内存空间不可用,即使jvm进行垃圾回收也无法有效回收垃圾。
4.1 Java语言将Entry设计为弱引用
设想:当线程内对ThreadLocal对象使用方法完毕后,此时没有对象指向ThreadLocal对象,按理说这个对象可以被回收了。但是由于ThreadLocalMap里以key,value的形式存储了ThreadLocal对象。导致仍然有链接指向ThreadLocal对象,不能被回收。
因此,Java语言将Entry类设置为弱引用,当线程内对ThreadLocal对象使用方法完毕后,JVM可以在垃圾回收时,清除ThreadLocalMap内的无效Entry对象。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
4.2 记得手动remove
由于ThreadLocalMap的生命周期和Thread(当前线程)一样长。
尽管java语言做出了良好的设计,但是若当前线程一直不结束,又或者由于线程在线程池中,结束后不被销毁,那么ThreadLocal被回收后,ThreadLocalMap就会存在null,但value不为null的Entry。无效数据会移植存储在内存中,无法回收,导致内存泄露。
所以在使用完ThreadLocal对象后,要及时调用ThreadLocal.remove()方法,手动删除对应value。
总结
ThreadLocal的设计是为了能够在当前线程中有属于自己的变量,其原理是每个线程内部其实都存储了一个ThreadLocalMap来记录保存。ThreadLocal对象存在内存泄露的风险,需要手工remove。
多线程系列在github上有一个开源项目,主要是本系列博客的实验代码。
https://github.com/forestnlp/concurrentlab
如果您对软件开发、机器学习、深度学习有兴趣请关注本博客,将持续推出Java、软件架构、深度学习相关专栏。
您的支持是对我最大的鼓励。