java 线程安全解决方案-java怎么解决线程安全问题
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());
     }
 }从上面可以看出,单个线程多次调用tl.get会返回相同的nextId,而不同的线程需要重新调用ThreadLocal.withInitial方法来赋值一个nextId值。 nextId值在多线程下不重复,说明nextId不在线程间共享变量。
ThreadLocal使用场景
线程隔离的ThreadLocal在实际项目中用的并不多,但是我们可以简单举几个常用但被忽略的业务场景
弹簧事务管理
为了保证单个线程中的数据库操作使用同一个数据库链接,spring可以通过事务传播来管理多个事务配置之间的切换,只要知道数据库链接是由ThreadLock管理的即可。
 public abstract class TransactionSynchronizationManager {
 
      //线程上下文中保存着【线程池对象:ConnectionHolder】的Map对象。线程可以通过该属性获取到同一个Connection对象。
     private static final ThreadLocal正确使用 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类的格式实现类中,有如下操作
 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();

          * 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是对象。 施工图应如下所示。
代码语义如下
 public class MyThreadLock {
     // 容器
     private Map 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中也有一个名为ThreadLocalMap的Map,但是持有者不是ThreadLocal类,而是Thread类。 Thread类有属性threadLocals,其类型为ThreadLocalMap,ThreadLocalMap的key类型为ThreadLocal
结构图如下
源码说明
 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

     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会被回收。 这时候,即使值的生命周期已经结束,值对象也无法被回收,从而导致内存泄漏。
既然有内存泄漏的风险,那怎么解决呢?
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对象后,在主线程内部创建的所有线程(即子线程)都可以获得该值。

源码分析
 // 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,可能会导致业务问题。 逻辑计算错误比内存泄漏更难解决。

 
  
  
  当前位置:
当前位置:  上一篇
上一篇  
   
        
 
                 
                 
                 
              
              
             
              
              
             
              
              
            