当前位置: 主页 > JAVA语言

java 线程安全解决方案-java怎么解决线程安全问题

发布时间:2023-02-08 16:11   浏览次数:次   作者:佚名

ThreadLocal本地存储保证并发安全前言介绍

线程由于并发执行带来了性能优势,同时,多线程之间的数据竞争也带来了线程安全问题。 之前提到过,不可变类Immutability可以用来解决线程安全问题。 该方法的本质是让Threads不直接修改属性值,保证线程安全。 其实还有一种方式就是线程不共享,读写自己线程的变量。 没有分享,就没有伤害。 这就是本地存储方案ThreadLocal的优势所在。

什么是 ThreadLocal

ThreadLocal其实是一种线程关闭的思想。 本质上有点像局部变量。 方法入栈时,所有的局部变量都存储在栈帧的局部变量表中。 它们是当前线程独有的,不会与其他线程共享。 这样保证了线程安全,对于ThreadLocal也是如此。 主要功能是数据隔离。 填充的数据只属于当前线程。 变量数据与其他线程相对隔离,保证多线程场景不会被其他线程篡改。 .

如何使用线程本地

 /**
  * 类的作用是给每一个线程分配一个id
  */
 class ThreadId{
     static final AtomicLong nextId = new AtomicLong(0);
 
     // withInitial 创建线程局部变量
     static final ThreadLocal tl = ThreadLocal.withInitial(()->{
        return nextId.getAndIncrement();
     });
 
     //返回当前线程的局部变量的副本中的值。 如果变量没有当前线程的值,则首先将其初始化
     //为调用initialValue()方法返回的值
     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());
     }
 }

java怎么解决线程安全问题_java 线程安全解决方案_线程死锁 解决

从上面可以看出,单个线程多次调用tl.get会返回相同的nextId,而不同的线程需要重新调用ThreadLocal.withInitial方法来赋值一个nextId值。 nextId值在多线程下不重复,说明nextId不在线程间共享变量。

ThreadLocal使用场景

线程隔离的ThreadLocal在实际项目中用的并不多,但是我们可以简单举几个常用但被忽略的业务场景

弹簧事务管理

为了保证单个线程中的数据库操作使用同一个数据库链接,spring可以通过事务传播来管理多个事务配置之间的切换,只要知道数据库链接是由ThreadLock管理的即可。

 public abstract class TransactionSynchronizationManager {
 
      //线程上下文中保存着【线程池对象:ConnectionHolder】的Map对象。线程可以通过该属性获取到同一个Connection对象。
     private static final ThreadLocal> resources = new NamedThreadLocal<>("Transactional resources");
 
     //事务同步器
     private static final ThreadLocal> synchronizations = new NamedThreadLocal<>("Transaction synchronizations");

java怎么解决线程安全问题_线程死锁 解决_java 线程安全解决方案

// 事务名称 private static final ThreadLocal currentTransactionName = new NamedThreadLocal<>("Current transaction name"); // 事务是否是只读 private static final ThreadLocal currentTransactionReadOnly = new NamedThreadLocal<>("Current transaction read-only status"); // 事务的隔离级别 private static final ThreadLocal currentTransactionIsolationLevel = new NamedThreadLocal<>("Current transaction isolation level"); // 事务是否开启 actual:真实的 private static final ThreadLocal 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();
         }
     }
 }

预期的结果应该是这样的

线程死锁 解决_java 线程安全解决方案_java怎么解决线程安全问题

实际结果是这样的

java怎么解决线程安全问题_线程死锁 解决_java 线程安全解决方案

原因分析如下。 在SimpleDateFormat类的格式实现类中,有如下操作

 public class SimpleDateFormat extends DateFormat {
    // DateFormat继承过来的
    protected Calendar calendar;
     
    private StringBuffer format(Date date, StringBuffer toAppendTo,
                                 FieldDelegate delegate) {
         // Convert input date to time field list
         // 多个线程之间共享变量calendar,并修改calendar
         calendar.setTime(date);
         // 代码省略
     }
 }

怎么解决呢?其实有很多方法如下

 public class ThreadLockDemo2 {
     public static void main(String[] args) throws ParseException {
         /**
          * 注意点,在创建ThreadLocal对象时 如果赋值只是如下这种方式赋值,那么只有当前线程
          * 调用threadLocal.get()能够获取到,其它线程一律是空!!!!
          * ThreadLocal threadLocal = new ThreadLocal();

线程死锁 解决_java怎么解决线程安全问题_java 线程安全解决方案

* threadLocal.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); */ ThreadLocal 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(); } } }

上下文参数传递

如果项目中有一个线程需要跨多个方法调用java 线程安全解决方案,那么需要传递的对象就是上下文(context)。 如果采用责任链的形式java 线程安全解决方案,会很麻烦,需要为每个方法添加上下文参数,使用ThreadLock可以轻松解决。

 class Test{
     // before
     public void work(){
         getInfo(user);
         checkInfo(user);
         setSomeThing(user);
         log(user);
     }
     
     // now
     public void work(){
         try{
             threadLocal.set(user);
             // 方法内部采用 user = threadLocal.get()即可获取
             getInfo(user);
             checkInfo(user);
             setSomeThing(user);
             log(user);
         }finally {
             threadLocal.remove();
         }
     }
 }

ThreadLocal 的工作原理 存储结构

了解了ThreadLocal的使用和应用场景之后,如果我们自己设计一个ThreadLocal应该怎么办呢?

存储容器应该是Map类型,key是线程,value是对象。 施工图应如下所示。

java 线程安全解决方案_java怎么解决线程安全问题_线程死锁 解决

代码语义如下

 public class MyThreadLock {
     // 容器
     private Map map = new ConcurrentHashMap<>();

线程死锁 解决_java 线程安全解决方案_java怎么解决线程安全问题

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中也有一个名为ThreadLocalMap的Map,但是持有者不是ThreadLocal类,而是Thread类。 Thread类有属性threadLocals,其类型为ThreadLocalMap,ThreadLocalMap的key类型为ThreadLocal

java 线程安全解决方案_线程死锁 解决_java怎么解决线程安全问题

结构图如下

java怎么解决线程安全问题_线程死锁 解决_java 线程安全解决方案

源码说明

 class Thread {
   // 内部持有ThreadLocalMap
   ThreadLocal.ThreadLocalMap threadLocals = null;
     
   ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
 }
 class ThreadLocal{
   // 获取线程Thread类的属性threadLocals 也就是 ThreadLocalMap
   ThreadLocalMap getMap(Thread t) {
      return t.threadLocals;
   }
   // 省略其它逻辑
   public T get() {
         Thread t = Thread.currentThread();
         // 获取根据当前线程ThreadLocalMap
         ThreadLocalMap map = getMap(t);
         if (map != null) {
             // this为当前调用get的ThreadLocal对象
             ThreadLocalMap.Entry e = map.getEntry(this);
             if (e != null) {
                 @SuppressWarnings("unchecked")
                 T result = (T)e.value;
                 return result;
             }
         }
         // 初始化ThreadLocalMap对象
         return setInitialValue();
   }
   static class ThreadLocalMap{
     // 内部是数组而不是Map

java 线程安全解决方案_java怎么解决线程安全问题_线程死锁 解决

Entry[] table; // 根据ThreadLocal查找Entry Entry getEntry(ThreadLocal key){ //省略查找逻辑 } //Entry定义 static class Entry extends WeakReference{ 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会被回收。 这时候,即使值的生命周期已经结束,值对象也无法被回收,从而导致内存泄漏。

线程死锁 解决_java 线程安全解决方案_java怎么解决线程安全问题

既然有内存泄漏的风险,那怎么解决呢?

JVM不能自动释放值对象,我们可以手动释放值对象。 一般使用try{}finally{}的方式来释放资源,ThreadLocal同样适用。 使用ThreadLocal后,手动清除当前线程的Entry对象。 能

 ThreadLocal threadLocal = new ThreadLocal();
 try {
     threadLocal.set("test");
     Object o = threadLocal.get();
 }finally {
     // 手动移除  原理很简单就是将当前对象在Entry[]数组中查找,做数组元素移除操作
     threadLocal.remove();
 }

如何共享 ThreadLock 数据

ThreadLock实现了线程关闭的思想,但是如果我想指定ThreadLock中的数据是线程间共享的,怎么处理呢?用InheritableThreadLocal实现,先上传代码

 public static void main(String[] args) {
     ThreadLocal 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对象后,在主线程内部创建的所有线程(即子线程)都可以获得该值。

线程死锁 解决_java怎么解决线程安全问题_java 线程安全解决方案

源码分析

 // ThreadLocal 类
 public void set(T value) {
     Thread t = Thread.currentThread();
     // 调用InheritableThreadLocal类的getMap 获取的是
     // Thread类的属性  ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
     ThreadLocalMap map = getMap(t);
     if (map != null)
         map.set(this, value);
     else
         // 重点在这里 在第一次没有获取到ThreadLocalMap 创建map 
         // 调用InheritableThreadLocal类的createMap
         createMap(t, value);
 }
 
 public class InheritableThreadLocal extends ThreadLocal {
     protected T childValue(T parentValue) {
         return parentValue;
     }
 
     ThreadLocalMap getMap(Thread t) {
        return t.inheritableThreadLocals;
     }
     // 重写createMap方法
     void createMap(Thread t, T firstValue) {
         t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
     }
 }
 
 public class Thread{
     
     ThreadLocal.ThreadLocalMap threadLocals = null;
     
     ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
     // inheritThreadLocals=true
     private void init(ThreadGroup g, Runnable target, String name,
                       long stackSize, AccessControlContext acc,
                       boolean inheritThreadLocals) {
         // 省略无数代码
         // parent指创建子线程的线程 从示例中看就是主线程main
         Thread parent = currentThread();
         // parent.inheritableThreadLocals 在threadLocal.set("牛逼");时已经不为空
         if (inheritThreadLocals && parent.inheritableThreadLocals != null)
             // 子线程的属性
             this.inheritableThreadLocals =
                 ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
         // 省略无数代码
     }
 }

需要注意的是,在生产中,尽量不要使用InheritableThreadLocal,不仅有内存泄漏的风险,还有线程池中的线程是动态创建的问题,容易造成继承关系的混乱。 如果业务逻辑依赖于InheritableThreadLocal,可能会导致业务问题。 逻辑计算错误比内存泄漏更难解决。