ThreadLocal本地存储保证并发安全
前言引入
多线程因为并发执行带来了性能上的优势,同时也因为多线程间的数据竞争导致线程安全问题,我之前有提过可以利用不变性类Immutability来解决线程安全问题,这个办法的本质是让线程不直接修改属性值来保证线程安全,其实还有一种办法那就是线程间不共享,各自读写各自线程的变量,没有共享便没有了伤害,这就是本地存储方案ThreadLocal的优势所在。
什么是ThreadLocal
ThreadLocal其实就是一种线程封闭的思想,本质有点像局部变量,所有的局部变量在方法入栈时保存在了栈帧的局部变量表内部,是当前线程所独有的不会共享给其它线程,这样就保证了线程安全,同样ThreadLocal也是如此主要作用是数据隔离,填充的数据只属于当前线程,变量的数据对其它线程而言是相对隔离的,保证了多线程场景不会被其它线程所篡改。
ThreadLocal 的使用方法
class ThreadId{
static final AtomicLong nextId = new AtomicLong(0);
static final ThreadLocal<Long> tl = ThreadLocal.withInitial(()->{
return nextId.getAndIncrement();
});
static long get(){
return tl.get();
}
}
方法执行结果对比
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i <5 ; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"===="+ThreadId.get());
},"T"+i).start();
}
Thread.sleep(3000);
System.out.println("=======================");
for (int i = 0; i <5 ; i++) {
System.out.println(Thread.currentThread().getName()+"===="+ThreadId.get());
}
}
从上面可以看出,单线程多次调用tl.get会返回相同的nextId,而不同线程则需要重新调用ThreadLocal.withInitial方法进行一次nextId的赋值,多线程下nextId值没有重复说明线程间没有共享nextId变量。
ThreadLocal 的使用场景
ThreadLocal 做线程隔离在实际项目中使用并不多,不过可以简单例举几种常用但是被忽略的业务场景
Spring事务管理
spring为了保证单个线程中的数据库操作使用的是同一个数据库链接,通过事务传播可以管理多个事务配置之间切换等,只要知道数据库链接是采用ThreadLock管理即可。
public abstract class TransactionSynchronizationManager {
private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations = new NamedThreadLocal<>("Transaction synchronizations");
private static final ThreadLocal<String> currentTransactionName = new NamedThreadLocal<>("Current transaction name");
private static final ThreadLocal<Boolean> currentTransactionReadOnly = new NamedThreadLocal<>("Current transaction read-only status");
private static final ThreadLocal<Integer> currentTransactionIsolationLevel = new NamedThreadLocal<>("Current transaction isolation level");
private static final ThreadLocal<Boolean> actualTransactionActive = new NamedThreadLocal<>("Actual transaction active");
}
SimpleDataFormat正确使用
SimpleDataFormat之前出现过多线程下的线程安全问题,如下
public class ThreadLockDemo2 {
public static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) throws ParseException {
for (int i = 0; i <10 ; i++) {
int finalI = i;
new Thread(()->{
String format1 = simpleDateFormat.format(new Date(finalI *1000));
System.out.println(Thread.currentThread().getName()+"=="+format1);
},"T"+i).start();
}
}
}
预期结果应该如下所示
实际结果如下所示
原因分析如下,在SimpleDateFormat类的format实现类中有如下操作
public class SimpleDateFormat extends DateFormat {
protected Calendar calendar;
private StringBuffer format(Date date, StringBuffer toAppendTo,
FieldDelegate delegate) {
calendar.setTime(date);
}
}
如何解决呢?方法其实很多如下
- SimpleDateFormat共享影响不共享就好了,将SimpleDateFormat变为局部变量。
- 加锁synchronized执行format方法。
- 采用1.8提供的线程安全工具类DateTimeFormat。
- 采用ThreadLock做数据隔离,代码如下。
public class ThreadLockDemo2 {
public static void main(String[] args) throws ParseException {
ThreadLocal<SimpleDateFormat> threadLocal = ThreadLocal.withInitial(()->{
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
});
for (int i = 0; i <10 ; i++) {
int finalI = i;
new Thread(()->{
SimpleDateFormat simpleDateFormat = threadLocal.get();
String format1 = simpleDateFormat.format(new Date(finalI *1000));
System.out.println(Thread.currentThread().getName()+"=="+format1);
},"T"+i).start();
}
}
}
上下文参数传递
在项目中如果存在一个线程需要横跨若干方法调用,需要传递的对象也就是上下文(context),如果采用责任链的形式那么非常麻烦需要给每一个方法增加context参数,而采用ThreadLock就可以轻松解决。
class Test{
public void work(){
getInfo(user);
checkInfo(user);
setSomeThing(user);
log(user);
}
public void work(){
try{
threadLocal.set(user);
getInfo(user);
checkInfo(user);
setSomeThing(user);
log(user);
}finally {
threadLocal.remove();
}
}
}
ThreadLocal 的工作原理
存储结构
了解到ThreadLocal的使用和应用场景后如果我们自己设计一个ThreadLocal应该怎么做呢?
存储容器应该是Map类型,key为线程,value为对象,构造图应该如下所示。
代码语义化如下
public class MyThreadLock<T> {
private Map<Thread,T> map = new ConcurrentHashMap<>();
public void set(T object){
Thread thread = Thread.currentThread();
map.put(thread,object);
}
public Object get(Thread thread){
return map.get(thread);
}
public void remove(){
map.clear();
}
}
但是JAVA真是这样实现的吗?显然并不是的,虽然JAVA中也存在一个Map名为ThreadLocalMap,但持有者并不是ThreadLocal类,而是Thread类,Thread类有属性threadLocals,其类型就是ThreadLocalMap,ThreadLocalMap的key类型为ThreadLocal
结构图如下所示
源码说明
class Thread {
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
class ThreadLocal<T>{
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
static class ThreadLocalMap{
Entry[] table;
Entry getEntry(ThreadLocal key){
}
static class Entry extends WeakReference<ThreadLocal>{
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
}
从源码中也能解释为什么ThreadLocal能线程安全了,因为ThreadLocal的值存储在当前线程Thread类的ThreadLocalMap类型的threadLocals属性中,每个线程拥有自己的ThreadLocalMap互不干扰。
同时这里需要解释下WeakReference弱引用,什么是WeakReference弱引用呢?
顾名思义指在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了==只具有弱引用==的对象,不管当前内存空间足够与否,都会回收它的内存。
内存泄露风险
由于ThreadLocalMap和Thread的同生共死的,所以ThreadLocalMap一直不会回收,而ThreadLocal又是弱引用,当外界没有强引用ThreadLock对象后Entry对象中的key就会回收,而Entry中的value却被Entry强引用这时就算value的生命周期已经结束其实也无法回收value对象,从而造成内存泄露。
既然出现了内存泄露的风险那么如何解决呢?
JVM无法做到自动释放value对象那我们可以手动释放value对象即可,一般释放资源都是采用try{}finally{}的模式,对于ThreadLocal同样适用,在使用完ThreadLocal手动清空当前线程的Entry对象即可
ThreadLocal threadLocal = new ThreadLocal();
try {
threadLocal.set("test");
Object o = threadLocal.get();
}finally {
threadLocal.remove();
}
怎么共享ThreadLock数据
ThreadLock做到了线程封闭的思想,但如果我就想指定线程间共享ThreadLock中的数据这个怎么处理呢?采用InheritableThreadLocal实现,先上代码
public static void main(String[] args) {
ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
try {
threadLocal.set("牛逼");
new Thread(()->{
System.out.println("牛逼不牛逼:"+threadLocal.get());
new Thread(()->{
System.out.println("子线程牛逼波:"+threadLocal.get());
}).start();
}).start();
}finally {
threadLocal.remove();
}
}
在主线程中创建ThreadLocal对象后,在主线程内部创建的所有线程(也就是子线程)都能获取到值。
源码分析
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
protected T childValue(T parentValue) {
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
public class Thread{
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
Thread parent = currentThread();
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
}
需要注意的是生产下还是尽量不使用InheritableThreadLocal,不仅仅是有内存泄露的风险还有一个问题是线程池中的线程是动态创建的,容易造成继承关系混乱,如果业务逻辑依赖InheritableThreadLocal,可能造成业务逻辑计算错误,这个比内存泄露更加难以排查。
|