美文网首页
Java基础-浅析解决并发的几种方式

Java基础-浅析解决并发的几种方式

作者: 九心_ | 来源:发表于2019-03-11 18:16 被阅读0次

    前言

    在上一篇中,我们讨论了Java中的关键字volatilesynchronized

    Java基础-浅谈关键字volatile和synchronized

    那么我们可以再想想,除了synchronized我们还有什么解决并发的方式呢?

    目录

    目录

    除了我们目录里面,还有其他的解决并发的方式,如读写锁等,这里不作介绍。

    一. synchronized

    请看我的上一篇文章,这里不再赘述。

    Java基础-浅谈关键字volatile和synchronized

    二. 锁对象

    在JDK 1.5中,出现了ReentrantLock类,为了方便使用,先看一下它的构造函数:

    public ReentrantLock(boolean fair) {
        // fair代表着是否是公平锁
        // 1. 如果是公平锁,当获取锁的时候,先来的线程会先获取到锁
        // 2. 如果不是公平锁,不用排队,直接获取锁
        sync = fair ? new FairSync() : new NonfairSync();
    }
    

    (1) 使用

    private ReentrantLock myLock = new ReentrantLock(false);
    
    private void doSomeThing(){
        // 线程如果想访问锁里面的代码
        // 必须得先获取锁,如果锁被之前的线程还没有释放锁,那么线程就会阻塞,
        // 直到之前的线程释放锁
        myLock.lock();
        try{
            ...
        }finally{
            // 处理完了记得释放锁
            myLock.unlock();
        }
    }
    

    (2) 注意

    • 听起来公平锁要合理的多,不过公平锁要比常规锁慢的多,通常还是不建议使用公平锁的。
    • synchronized类似,线程可以重复地获得已经持有地锁,锁会用一个持有计数来跟踪线程对lock()方法的嵌套调用。

    三. 原子操作类

    1. CAS(compare and set)

    大家应该都很熟悉AtomicXXX了,除了使用synchronized,这应该是实现原子操作最常用的一种方式。AtomicXXX的是一种乐观锁,每次去修改数据的时候都会认为别人没有在更新数据,等到要更新结果的时候再去比对值,确定值没有被修改的前提下再更新值。

    (1) 使用

    AtomicXXX下面有很多种类型,比如AtomicBooleanAtomicLongAtomicReference等,这里以 AtomicInteger为例:

    private AtomicInteger num = new AtomicInteger(3);
    ...
    // observed是一个int类型的数字
    private void max() {
        int oldValue = num.get();
        int newValue = Math.max(oldValue, observed);
        num.set(oldValue);
    }
    

    你以为这样就实现原子操作了?非也非也,其实这是个错误的示范,那我们看一下正确的使用姿势:

    private AtomicInteger num = new AtomicInteger(3);
        
    // observed是一个int类型的数字
    private void add() {
        int oldValue,newValue;
        do {
              // 1. 先获取旧的值
              // 2. 比较大小
              // 3. 检查num是否发生过更改
              // 4. 如果num发生变化,就意味着我们的操作失败,重复执行如上1-3操作
              // 5. 如果num没发生变化,就更新当前的值
          oldValue = num.get();
          newValue = Math.max(oldValue, observed);
        }while(!num.compareAndSet(oldValue, newValue));
    }
    

    (2) 注意

    相比synchronizedReentrantLock,乐观锁会减去线程挂起和恢复的开销,提升系统的运行效率。当然了,乐观锁也会有缺点,当线程的并发数量上来的时候,大量的线程操作相同的原子值,乐观更新的失败几率比较高,可能需要重复多次,因此,乐观锁适用于多读的系统。

    2. Java 8新增的xxxAdder和xxxAccumulator

    Java 8新增的这些原子操作类就是为了解决上述乐观锁的并发问题的

    (1) 使用

    LongAdder是以LongAccumulator为基础的,LongAdder只能使用累加,并且初始值只能设置为0,相比之下,LongAccumulator可以实现更复杂的运算和设置初始值。这里以LongAccumulator为例:

    // 先自定义实现LongBinaryOperator
    class MyOp implements LongBinaryOperator{
        @Override
        public long applyAsLong(long left, long right) {
          return left + right;
        }
    }
    
    // 第一个参数是处理自定义实现的处理方法
    // 第二个参数就是LongAccumulator累加器的初始值
    private LongAccumulator num = new LongAccumulator(new MyOp(), 2);
        
    private void add(int observed) {
        num.accumulate(observed);
    }
    

    (2) 使用场景

    我们来看一下《Java核心技术卷》是怎么说的:

    LongAdder包括多个变量(加数),其总和为当前值。可以有多个线程更新不同的加数,线程个数增加时会自动提供新的加数。通常情况下,只有当所有工作都完成之后才需要总和的值,对于这种情况,这种方法会很高效。

    因此,LongAdderLongAccumulator适合高并发下的计数问题。

    (3) 注意

    看到这里,你可能会有这样的想法,XXXAdderXXXAccumulator这么厉害了,是不是意味着我们可以抛弃AtomicXXX了?当然不可能,XXXAdderXXXAccumulator只适合高并发下的计数问题,除此之外,LongAccumulator
    也使用了CAS的方式处理数据。

    四. 总结

    总结

    总结得出来的使用技巧是:

    1. 优先考虑synchronized,需要性能调优的时候考虑Lock
    2. 优先使用传统的互斥方式,当性能方面的需求有明确指示的时候,考虑Atomic
    3. 高并发计数优先考虑XXXAdderXXXAccumulator

    本人水平有限,难免会有错误,如有错误,欢迎提出。
    Over~

    引用
    浅析LongAdder

    相关文章

      网友评论

          本文标题:Java基础-浅析解决并发的几种方式

          本文链接:https://www.haomeiwen.com/subject/lyjqpqtx.html