SpringBoot——使用ThreadLocal解决类成员变量并发线程安全问题!
问题
在开发过程中,我们一旦在某个类中使用一个可变的成员变量,就会涉及到线程安全问题,因为我们的类对于其他依赖使用类来说,可能是单例注入的,这就会涉及到多个线程共享操作同一个变量问题。如何解决?
遇到线程安全问题,我们首先想到的就是使用锁
,万物可加锁,只要不怕慢!我们通过加锁来实现多个线程并发访问操作问题,我加锁,你就得等我解锁后才能操作。但是众所周知,加锁,必定会在多线程并发访问时造成一部分线程阻塞等待,从而产生一定的性能影响。那除了加锁,有没有其他方法来避免?答案是:有滴!我们可以使用多种方式,下面我们娓娓道来~
ThreadLocal方式
介绍
ThreadLocal
从字面理解就是本地线程,全称:Thread Local Variable
。换句话说,就是当前线程变量,它是一个本地线程变量,其填充的是当前线程
的变量,这个变量对于其他线程来说都是封闭且隔离的
。- 如何实现变量隔离这一功能?
ThreadLocal
可以为每个线程创建一个自有副本,每个线程可以访问自己内部的副本变量来达到隔离效果,从而解决共享变量的线程安全问题。 ThreadLocal
变量是线程内部的局部变量,在不同的线程Thread中有不同的副本,副本只能由当前Thread使用,不存在多线程共享问题。ThreadLocal
一般由private static
修饰,线程结束时,可回收掉ThreadLocal
副本。
案例
之前在SpringBoot—集成AOP详解(面向切面编程Aspect)中的AOP编码中也是用到了ThreadLocal
进行starttime
变量的存储。
源码
set方法
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程中的变量map
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
//若为空,则初始化当前线程的变量map,key为当前线程,map为变量
createMap(t, value);
}
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
//弱引用,若为null时,ThreadLocal被回收,但是map的value还存在,容易造成内存泄漏
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
//使用Entry保存数据,k为ThreadLocal,v为变量value值
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
/**
* The number of entries in the table.
*/
private int size = 0;
...
//其他源码省略
...
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
}
ThreadLocalMap
是ThreadLocal
的一个静态内部类,使用Entry
保存数据。Entry
继承WeakReference
弱引用,key为当前线程ThreadLocal
,value为变量值。
get方法
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取变量map
ThreadLocalMap map = getMap(t);
if (map != null) {
//从获取Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
内存泄漏问题
原因
ThreadLocal
是弱引用,若为null
时,ThreadLocal
被回收(这样可以避免Entry
内存泄漏)。- 虽然
ThreadLocalMap
保存的ThreadLocal
弱引用被回收了,但的value
还存在,容易造成内存泄漏。
引用
强引用
:强引用的对象,不会被回收。如直接new
一个对象,就算OOM异常,也不会回收该对象。软引用
:软引用的对象,只有发生gc时,发现内存不足,才会回收。如缓存,系统内存充足时,一般不会回收对象,当系统内存不足且gc时,就会回收这些软引用对象。弱引用
:弱引用的对象,只要发生gc,就会被回收。虚引用
:虚引用一般和引用队列联合使用,对象若持有虚引用,就等于没有任何引用,在任何时候都可能被gc,主要用来跟踪对象被gc的活动。
解决方案
在使用完ThreadLocal
的时候,最后使用remove()
方法进行当前线程变量值的移除。
使用场景
- 线程间数据隔离,每个线程创建自己的ThreadLocal变量副本。
- 进行事务操作,用于存储线程事务信息。
- 数据库连接,进行Session会话管理。
- 解决多线程中数据并发不一致的问题。
ThreadLocal和synchronized区别
synchronized
是基于锁机制,在某一时刻,变量或者代码只能由一个线程进行访问,有上锁和解锁的边界,用于解决多个线程之间的数据共享竞争问题。ThreadLocal
是基于每个线程有独立的变量副本,每个线程在同一时刻都可以访问变量,可并发访问,只不过多个线程访问到的变量不是同一个,是各自线程内独立的副本,用于解决多个线程需隔离数据共享的问题。
使用方式
代码示例
public static ThreadLocal<String> local = new ThreadLocal<>();
public static void main(String[] args) {
try {
LongStream.range(100000000, 100000005)
.forEach(a -> new Thread(()-> {
local.set(Thread.currentThread().getName() + "-" + a);
System.out.println("线程名称:" + Thread.currentThread().getName() + ", local: " + local.get());
}).start());
} finally {
local.remove();
}
}
运行结果
线程名称:Thread-0, local: Thread-0-100000000
线程名称:Thread-3, local: Thread-3-100000003
线程名称:Thread-2, local: Thread-2-100000002
线程名称:Thread-1, local: Thread-1-100000001
线程名称:Thread-4, local: Thread-4-100000004
我们可以从运行结果中看出,local是跟随Thread独立的。
@Scope多例注解方式
介绍
- 使用
@Scope("prototype")
注解,解决Bean的多例问题,替代性的解决多线程类成员变量共享问题。 - 在使用Spring的IOC功能来管理Bean时,默认是单例的,在多线程下,类的成员变量如果是个可变的值,则会有线程安全问题。需要的时候,我们可以直接拿来即用,使用
@Autowired
或@Resource
注解注入即可。
使用
- Service层使用格式
@Service
@Scope("prototype")
public class XxxService {
}
- 上层注入使用
需要区分上层是否也会是单例。建议使用@Resource
注解注入。
烧不死的鸟就是凤凰