简单介绍
开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第4天,点击查看活动详情
线程安全的解决方案
什么是线程安全?
「线程安全」是指广义上的共享资源访问安全性,因为线程隔离是通过副本保证本线程访问资源安全性,它不保证线程之间还存在共享关系的狭义上的安全性
线程安全的实现思路:
- 互斥同步:
synchronized和ReentrantLock - 非阻塞同步:
CAS,AtomicXXXX - 无同步方案: 栈封闭,本地存储(Thread Local),可重入代码
于是本章我们就来详细说说本地存储(Thread Local)如何实现线程安全
什么是 ThreadLocal?
什么是
ThreadLocal?
ThreadLocal是一个关于创建线程局部变量的类。
通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。
而使用ThreadLocal创建的变量只能被当前线程访问,其他线程则无法访问和修改。
这意味着,每个线程都可以通过ThreadLocal类来维护它自己的局部变量。这些变量对于每个线程都是独立的,因此线程之间不会相互影响。 所以避免了因多线程操作共享变量而导致的数据不一致的情况。
ThreadLocal 原理
如何实现线程隔离?
ThreadLocal能做到线程隔离,那么是如何实现的呢?
主要是用到了Thread对象中的一个ThreadLocalMap类型的变量threadLocals, 负责存储当前线程的指定类型的对象。
public class Thread implements Runnable {
// ...
ThreadLocal.ThreadLocalMap threadLocals = null;
// ...
}
ThreadLocal源码中具体关于为线程分配变量副本的代码如下:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap threadLocals = getMap(t);
if (threadLocals != null) {
ThreadLocalMap.Entry e = threadLocals.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
解释:
- 首先获取当前线程对象 t , 然后从线程 t 中获取到
ThreadLocalMap的成员属性threadLocals - 如果当前线程的
threadLocals已经初始化(即不为null) 并且存在以当前ThreadLocal对象为Key的值, 则直接返回当前线程要获取的对象(本例中为Connection); - 如果当前线程的
threadLocals已经初始化(即不为null)但是不存在以当前ThreadLocal对象为Key的的对象, 那么重新创建一个Connection对象, 并且添加到当前线程的threadLocals Map中,并返回 - 如果当前线程的
threadLocals属性还没有被初始化, 则重新创建一个ThreadLocalMap对象, 并且创建一个Connection对象并添加到ThreadLocalMap对象中并返回。
接着我们看setInitialValue方法
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
-
首先调用我们上面写的重载过后的
initialValue方法, 产生一个指定类型的对象 -
通过
getMap(t)获取 t 线程的threadLocals,继续查看当前线程的threadLocals是不是空的, 如果ThreadLocalMap已被初始化, 那么直接将产生的对象添加到ThreadLocalMap中, 如果没有初始化, 则调用createMap创建并添加对象到其中;createMap方法源码如下:void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
查看源码后我们还发现initialValue是一个「空方法」,需要我们重载才有意义。
protected T initialValue() {
return null;
}
但是也有不需要重载initialValue也可以有效的获取指定对象的解决方案
ThreadLocal提供了**直接操作Thread对象中的threadLocals**的方法:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
这样我们也可以不实现initialValue, 将初始化工作放到DBConnectionFactory的getConnection方法中:
public Connection getConnection() {
Connection connection = dbConnectionLocal.get();
if (connection == null) {
try {
connection = DriverManager.getConnection("", "", "");
dbConnectionLocal.set(connection);
} catch (SQLException e) {
e.printStackTrace();
}
}
return connection;
}
看过源码后就知道了为什么ThreadLocal能够实现变量的多线程隔离了;
我们不难发现创建或者缓存数据到threadLocals的核心代码基本上都是:
map.set(this, value);
其实就是用了Map的数据结构给当前线程缓存了, 要使用的时候就从本线程的threadLocals对象中获取就可以了, key就是当前线程;
在当前线程下获取当前线程里面的Map里面的对象并操作肯定没有线程并发问题了, 当然能做到变量的线程间隔离了;
ThreadLocalMap
什么是
ThreadLocalMap?
ThreadLocalMap是ThreadLocal的一个静态内部类
这个类的构成主要是用Entry来保存数据,而且还是继承的弱引用,用一个键值对来存储,键就是ThreadLocal的引用。每一个线程都有一个ThreadLocalMap的对象,每一个新的线程Thread都会实例化一个ThreadLocalMap并赋予值给成员变量threadLocals。
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的实现, 我们先从入口开始, 也就是set方法:
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
解释:
-
看下当前
threadLocal的在数组中的索引位置比如:
i = 2, 看i = 2位置上面的元素(Entry)的Key是否等于threadLocal这个Key, 如果等于就很好说了, 直接将该位置上面的Entry的Value替换成最新的就可以了; -
如果当前位置上面的
Entry的Key为空, 说明ThreadLocal对象已经被回收了, 那么就调用replaceStaleEntry -
如果清理完无用条目(
ThreadLocal被回收的条目)、并且数组中的数据大小 > 阈值的时候对当前的Table进行重新哈希 所以, 该HashMap是处理冲突检测的机制是向后移位, 清除过期条目 最终找到合适的位置;
了解完set方法, 后面就是get方法了:
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
解释:
- 先找到
ThreadLocal的索引位置 - 如果索引位置处的
entry不为空并且键与threadLocal是同一个对象, 则直接返回;否则去后面的索引位置继续查找。
ThreadLocal 应用
快速开始
最简单的应用
首先创建ThreadLocal的实例threadLocal,然后用这个实例调用set和get方法,来设置和获取当前线程的局部变量。
ThreadLocal<Type> threadLocal = new ThreadLocal<>();
threadLocal.set(myValue);
Type value = threadLocal.get();
我们还可以用ThreadLocal.withInitial方法来给线程局部变量设置初始值。
ThreadLocal<Type> threadLocal = ThreadLocal.withInitial(() -> myInitialValue);
使用完 ThreadLocal 实例后,应调用 remove 方法删除当前线程的值并避免内存泄漏:
threadLocal.remove();
ThreadLocal具体有什么有应用场景?
ThreaLocal作用在每个线程内都都需要独立的保存信息,这样就方便同一个线程的其他方法获取到该信息的场景。
由于每一个线程获取到的信息可能都是不一样的,前面执行的方法保存了信息之后,后续方法可以通过ThreadLocal可以直接获取到,避免了传参,这个类似于全局变量的概念。
知道了这个特性后,接着我们就来讲讲ThreadLocal的常见应用吧。
数据库连接
场景
如果您有一个应用程序,其中包含多个线程,并且每个线程都需要使用相同的数据库连接,则可以使用 ThreadLocal 来维护每个线程的数据库连接。这样,每个线程都可以使用它自己的数据库连接,而不会与其他线程发生冲突。
-
具体一些?
在我们平常的
SpringWeb项目中,我们通常会把业务分成Controller、Service、Dao等等,注解@Autowired默认使用单例模式。当不同的请求线程进来后,因为
Dao层使用的是单例,那么负责连接数据库的Connection也只有一个,这时候如果请求的多个线程都去连接数据库的话,就会造成这个线程不安全的问题。(多个线程竞争共享资源) -
Spring如何解决数据库连接线程不安全的问题?Spring框架通过提供一个统一的数据访问接口来解决数据库连接的线程安全问题。具体来说,Spring提供了一个DataSource,它定义了获取数据库连接的方法。应用程序通过调用这个接口的方法来获取数据库连接,而不需要直接与数据库打交道。Spring框架会在后台为每个线程维护一个数据库连接,并通过ThreadLocal来实现线程安全。Dao层里装配的Connection线程肯定是安全的,解决方案就是使用ThreadLocal方法。当每一个请求线程使用Connection的时候,都会从ThreadLocal获取一次,如果值为null,那就说明没有对数据库进行连接,连接后就会存入到ThreadLocal里,这样一来,每一个线程都保存有一份属于自己的Connection。每一线程维护自己的数据,达到线程的隔离效果。(每个线程都有自己的数据库连接,因此没有获取共享数据的冲突)
-
例子
下面是一个数据库管理类,它在单线程中使用是没有问题的
class ConnectionManager { private static Connection connect = null; public static Connection openConnection() { if (connect == null) { connect = DriverManager.getConnection(); } return connect; } public static void closeConnection() { if (connect != null) connect.close(); } }但是它在多线程中使用会存在线程安全问题:
- 第一,这里面的2个方法都没有进行同步,很可能在
openConnection方法中会多次创建connect; - 第二,由于
connect是共享变量,那么必然在调用connect的地方需要使用到同步来保障线程安全,因为很可能一个线程在使用connect进行数据库操作,而另外一个线程调用closeConnection关闭链接。
-
如何解决上述问题?
考虑互斥同步,将这段代码的两个方法进行同步处理,并且在调用
connect的地方需要进行同步处理,比如用Synchronized或者ReentrantLock互斥锁。接着就是考虑是否需要将
connect变量进行共享? 事实上,是不需要的。假如每个线程中都有一个connect变量(每个线程都创建一个自己的实例),各个线程之间对connect变量的访问实际上是没有依赖关系的,即一个线程不需要关心其他线程是否对这个connect进行了修改的。class ConnectionManager { private Connection connect = null; public Connection openConnection() { if (connect == null) { connect = DriverManager.getConnection(); } return connect; } public void closeConnection() { if (connect != null) connect.close(); } } class Dao { public void insert() { ConnectionManager connectionManager = new ConnectionManager(); Connection connection = connectionManager.openConnection(); // 使用connection进行操作 connectionManager.closeConnection(); } }由于每次都是在方法内部创建的连接,那么线程之间自然不存在线程安全问题。
但是这样会有一个致命的影响:导致服务器压力非常大,并且严重影响程序执行性能。由于在方法中需要频繁地开启和关闭数据库连接,这样不仅严重影响程序执行效率,还可能导致服务器压力巨大。
那么这种情况下使用
ThreadLocal是再适合不过的了,因为ThreadLocal在每个线程中对该变量会创建一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。下面就是应用
ThreadLocal创建数据库连接的例子:import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class ConnectionManager { private static final ThreadLocal<Connection> dbConnectionLocal = new ThreadLocal<Connection>() { @Override protected Connection initialValue() { try { return DriverManager.getConnection("", "", ""); } catch (SQLException e) { e.printStackTrace(); } return null; } }; public Connection getConnection() { return dbConnectionLocal.get(); } }但是要注意,虽然
ThreadLocal能够解决上面说的问题,但是由于在每个线程中都创建了副本,所以要考虑它对资源的消耗,比如内存的占用会比不使用ThreadLocal要大。
- 第一,这里面的2个方法都没有进行同步,很可能在
保存用户会话
场景
在Web应用程序中,我们可以使用ThreadLocal来存储每个用户的信息,从而避免线程安全问题。
比如像用户登录令牌解密后的信息传递、用户权限信息、从用户系统中获取到的用户名:
// 用户服务配置token解密信息传递例子
public static ThreadLocal<LoginUser> threadLocal = new ThreadLocal<>();
//...{
LoginUser loginUser = new LoginUser();
loginUser.setId(id);
loginUser.setUsername(username);
threadLocal.set(loginUser);
//}
后续想要获取这些信息就很容易了,你可以在应用程序的任何方法中调用threadLocal的get方法获取这个用户的信息,这样也就方便我们的鉴权,查询是否在线等用户相关操作,同时也可以减少数据库读操作。
ThreadLocal 慎用的场景
哪些场景我们尽量不能使用
ThreadLocal呢?
-
线程池里线程调用
ThreadLocal:因为线程池里对线程的管理都是线程复用的方法,所以在线程池里线程非常难结束,更有可能的是永远不会结束。这就意味着线程的持续时间是不可估测的,甚至会与
JVM的生命周期一致。 -
在异步程序里:
ThreadLocal的参数传递是不可靠的,因为线程将请求发送后,不会在等待远程返回结果就继续向下运行了,真正的返回结果得到以后,可能是其它的线程在处理。 -
在使用完
ThreadLocal,推荐要调用一下remove方法,这样会防止内存溢出这种情况的发生,因为ThreadLocal为弱引用。如果ThreadLocal在没有被外部强引用的情况下,在垃圾回收的时候是会被清理掉的,如果是强引用那就不会被清理。
为什么
ThreadLocal是WeakReference呢?
-
如果是强引用的话
即使
ThreadLocal的值是为null,但是的话**ThreadLocalMap还是会有ThreadLocal的强引用状态,如果没有手动进行删除的话,ThreadLocal就不会被回收,这样就会导致Entry内存的泄漏** -
如果是弱引用的话
引用
ThreadLocal的对象被回收掉了,ThreadLocalMap还保留有ThreadLocal的弱引用,即使没有进行手动删除,ThreadLocal也会被回收掉。value在下一次的ThreadLocalMap调用set/get/remove方法的时候就会被清除掉。
小结
本章我们介绍了ThreadLocal用于实现线程安全靠的是实现了访问变量之间的线程隔离,并且从源码角度分析了它是如何实现线程隔离的,最后我们还介绍了ThreadLocal的基本使用,和应用场景。
本章参考: