• Java中使用ThreadLocal减少线程同步的规模


    多线程访问共享资源

    通常在多线程访问共享资源的场景会存在线程安全,临界区,竞争条件等问题,比如:

    @RestController
    public class StatController {
    
      static Integer c = 0;
    
      @RequestMapping("/stat")
      public Integer stat() {
        return c;
      }
    
      @RequestMapping("/add")
      public Integer add() throws InterruptedException {
        // 这里是临界区,sleep模拟IO等待
        Thread.sleep(100);
        c++;
        return 1;
      }
    }
    
    // ab -n 1000 -c 100 http://192.168.2.1:8080/add
    // 耗时1.398s
    // curl http://192.168.2.1:8080/stat
    // 864 
    

    当多个线程访问共享资源时会产生竞态条件,导致最终结果不一致,最终stat统计值为864而不是1000。

    使用同步锁机制保证线程安全

    可以使用synchronized关键词对临界区代码片段加锁,变并发为排队,解决数据不一致问题

    @RestController
    public class StatController {
    
      static Integer c = 0;
      
      // 为临界区添加同步锁
      synchronized static void  __add() {
        Thread.sleep(100);
        c++;
      }
    
      @RequestMapping("/stat")
      public Integer stat() {
        return c;
      }
    
      @RequestMapping("/add")
      public Integer add() {
        __add();
        return 1;
      }
    }
    
    // ab -n 1000 -c 100 http://192.168.2.1:8080/add
    // 耗时103.280s
    // curl http://192.168.2.1:8080/stat
    // 1000
    

    加锁简单粗暴,但是性能很差。由于每次请求都需要排队,吞吐量必然下降巨大。

    所以,非常不推荐使用锁机制。

    隔离线程资源,减少对“共享资源”的同步写入

    同步锁会极大降低性能,尤其是共享资源的访问IO还很高的时候,即同步IO很慢。

    所以要尽量降低同步写入共享资源的频率。

    最好每个线程只操作自己的资源,对于共享资源尽量少的去写入。

    不能完全避免同步,因为有时要收集各线程的计算结果。

    可以使用一个HashMap让各线程只操作自己的资源,然后获取统计时再进行计算汇总。

    @RestController
    public class StatController {
    
      static HashMap<Thread, Integer> map = new HashMap<>();
      
      // 初始化仍然需要同步写入共享map
      // 因为新增key-value可能扩容导致HashMap被覆盖
      synchronized static void __putIfAbsent() {
        map.putIfAbsent(Thread.currentThread(), 0);
      }
    
      @RequestMapping("/stat")
      public Integer stat() {
        return map.values().stream().reduce(Integer::sum).get();
      }
    
      @RequestMapping("/add")
      public Integer add() throws InterruptedException {
        Thread.sleep(100);
        __putIfAbsent();
        Integer v = map.get(Thread.currentThread());
        v++;
        map.put(Thread.currentThread(), v);
        return 1;
      }
    }
    
    // ab -n 1000 -c 100 http://192.168.2.1:8080/add
    // 耗时1.365s
    // curl http://192.168.2.1:8080/stat
    // 1000
    

    上述做法使用HashMap去隔离线程资源,仅对可能存在的Map扩容做同步处理。

    使用HashMap将不同线程的资源隔离,让每个线程只操作自己的数据是减少线程同步的一种策略。

    所谓线程不安全本质上是因为“共享资源”存在被覆盖的风险,即前者的操作结果被后者的操作结果覆盖了。

    使用ThreadLocal再实现一遍

    上述直接使用HashMap存在很多问题,比如性能,资源回收等。

    Java中使用ThreadLocal实现上述隔离逻辑,ThreadLocal内部实现了一个ThreadLocalMap来对线程资源做隔离。

    @RestController
    public class StatController {
      
      // 使用HashSet收集计算结果
      // 由于Integer是不可变类型,定义一个Val做可变引用
      static HashSet<Val<Integer>> set = new HashSet<>();
      
      // 添加Set存在扩容导致线程安全的风险
      synchronized static void addSet(Val<Integer> v) {
        set.add(v);
      }
      
      static ThreadLocal<Val<Integer>> c = new ThreadLocal<Val<Integer>>(){
        @Override
        protected Val<Integer> initialValue() {
          // 初始化时分别添加Set收集器和ThreadLocalMap
          Val<Integer> v = new Val<>();
          v.set(0);
          addSet(v);
          return v;
        }
      };
    
      @RequestMapping("/stat")
      public Integer stat() {
        return set.stream().map(Val::get).reduce(Integer::sum).get();
      }
    
      @RequestMapping("/add")
      public void add() throws InterruptedException {
        Thread.sleep(100);
        // ThreadLocal会自动给出当前线程的值
        Val<Integer> v = c.get();
        v.set(v.get() + 1);
      }
    }
    
    // ab -n 1000 -c 100 http://192.168.2.1:8080/add
    // 耗时1.406s
    // curl http://192.168.2.1:8080/stat
    // 1000
    

    ThreadLocal将资源和线程进行绑定,做到线程间资源隔离。

    上述案例比较特殊,还存在一个资源收集的过程,但很多时候不需要收集,也就没有同步动作。

    如果多个线程都需要同一类型的资源,虽然可以手动在线程内定义局部变量,但使用threadLocal只需要定义一次,然后系统自动绑定和分配,也是很方便的。

    为什么代码可以优化?

    代码之所以可以优化,是因为代码中存在没必要的操作。

    上述案例类似于分布式计算的Map和Reduce过程,没有必要将每一次运算结果都进行汇总。

    只要等待所有线程计算完毕后最终汇总一次就可以了。

    所以使用什么数据结构取决于具体的问题,我们需要理解数据结构的功能然后灵活使用。

  • 相关阅读:
    Mac电脑上的软件以及作用整理
    sed命令在mac和linux下的区别
    gin框架开启web跨域
    Mac下输入特殊字符的总结
    4月4悼念日 多数app 或者网站变成灰色的实现效果
    Mqtt 客户端多主题订阅
    paho.mqtt.golang--option.go 源码解析
    MQTT服务器(Broker)
    asdine/storm学习笔记
    vuex基本使用教程
  • 原文地址:https://www.cnblogs.com/Peter2014/p/12731735.html
Copyright © 2020-2023  润新知