JAVA多线程之ThreadLocal详解
概述
在java多线程编程中,大部分的变量都不是线程安全的,多个线程间同时使用同一个变量便会产生各种问题,我们可以使用synchronized等同步方案解决,但是同步是要牺牲一定性能的,很多情况下我们并不需要这种方案,于是便有了一种牺牲空间的方案:ThreadLocal变量
简介
ThreadLocal结构
ThreadLocal为每一个线程维护了一个副本变量,多个线程之间对各自副本变量的修改肯定不会产生相互的的影响
我们先来看一下ThreadLocal的结构图,做一下介绍
- 蓝色线框为一个Thread对象,每个Thread对象有一个ThreadLocalMap类型的成员属性threadlocals
- ThreadLocalMap可以看成一个HashMap,每个Entry的key是ThreadLocal对象,value是存储的副本变量,当前对象中定义了多少个ThreadLocal变量,当前线程的ThreadLocalMap可以有多个ThreadLocal变量即对应多个ThreadLocalMap.Entry
- ThreadLocal提供了get()和set()方法以获取和设置当前线程的副本变量
详解
使用示例
public class ThreadLocalDemo {
static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
threadLocal.set(10);
System.out.println(Thread.currentThread() + ": set threadlocal " + threadLocal.get());
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread() + ": " + threadLocal.get());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
threadLocal.set(20);
System.out.println(Thread.currentThread() + ": set threadlocal " + threadLocal.get());
}
}).start();
}
}
//执行结果
Thread[Thread-0,5,main]: set threadlocal 10
Thread[Thread-1,5,main]: set threadlocal 20
Thread[Thread-0,5,main]: 10
上面代码中,新建了两个线程,第一个线程设置ThreadLocal为10,sleep了1秒,在这期间第二个线程将ThreadLocal置为20,通过执行打印可以看到两个进程之间没有产生影响
通过get()方法看ThreadLocal结构
public T get() {
// 1.获取到当前线程
Thread t = Thread.currentThread();
// 2.获取当前Thread对象的ThreadLocalMa类型的成员属性 【1】
ThreadLocalMap map = getMap(t);
if (map != null) {
// 3.以当前ThreadLocal对象为key获取到对应的Entry 【2】
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
// 4.获取到Entry的value即当前ThreadLocal维护的当前线程的对应变量值
T result = (T)e.value;
return result;
}
}
// 5.不存在则调用初始化方法
return setInitialValue();
}
来看一下标注【1】是如何获取ThreadLocalMap的
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
很简单,传入的Thread也就是当前的Thread对象存在一个成员属性threadlocals,其类型为ThreadLocalMap,该类为ThreadLocal的内部类
再看标注【2】,key是this也就是当前的ThreadLocal对象,由此我们就能明白最开始的结构图中的结构关系了:
Thread对象有一个ThreadLocalMap类型的变量threadlocals,该类类似于HashMap,其存储了多个以ThreadLocal为key、要存储的变量为value的Entry
set()方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
set()方法先进行当前线程ThreadLocalMap的获取,若其为null,则new一个并将value赋值
remove()方法
我们先来看一下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;
}
}
源码中我们可以看到,ThreadLocalMap.Entry继承了WeakReference类,也就是每个Entry的key对于ThreadLocal对象的引用是弱引用。
为什么使用弱引用呢?
当我们不再需要使用ThreadLocal进行回收时,若使用了强引用则会因为某个线程中的ThreadLocalMap存在对该ThreadLocal的强引用导致无法回收,使用弱引用则不会有这个问题了。
ThreadLocal存在的内存泄漏问题:
虽然ThreadLocalMap.Entry使用了弱引用避免了ThreadLocal无法被回收,但是ThreadLocalMap.Entry的value却是使用的强引用,在key的ThreadLocal因为GC被回收变为null之后,其value仍然存在,若线程一直存活,那该value便会一直存在于线程,从而导致内存泄漏。
ThreadLocal对于内存泄漏的问题也有解决方案
private int expungeStaleEntry(int staleSlot) {...}
该方法中有一步是擦除ThreadLocalMap中key为null的Entry,但是该方法需要在调用get()、set()、remove()方法的时候才会触发,如果我们在用完ThreadLocal后没有做任何的调用或处理,那就会造成内存泄漏了,因此,在使用之后调用remove()方法手动触发这个机制,则会避免上述的问题,这便是remove()方法的主要作用了。
实践—使用ThreadLocal实现数据库连接池
public class DBPool {
public static ThreadLocal<Connection> connection = new ThreadLocal<>();
private static Stack<Connection> connectionPool = new Stack<>();
static {
// 加载数据库连接驱动
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
public static Connection getConnection(){
Connection c = null;
if(null != connection.get()){
// 当前ThreadLocal获取数据库连接
c = connection.get();
}else{
// 没有数据库连接需要从连接池中获取
if(!connectionPool.isEmpty()){
c = connectionPool.pop();
}else{
// 连接池中没有可用连接则新建连接
try {
c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test","root","root");
} catch (SQLException e) {
e.printStackTrace();
}
}
// 设置ThreadLocal的数据库连接
connection.set(c);
}
return c;
}
public static void closeConnection(){
// 连接放回连接池
connectionPool.push(connection.get());
// 擦除引用,防止多线程共用一个连接及内存泄漏
connection.remove();
}
}
在上面简单的数据库连接池实现中,通过使用ThreadLocal的使用实现了每个线程独用一个数据库连接,多个线程之间不会因为使用了同一个连接导致事务混乱,但是要注意必须及时closeConnection来释放连接,closeConnection后也就擦除了对该连接对象的引用并归还了连接,其他线程再次获取到该链接的时候也不会存在某个线程还在使用该连接了。