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

2019-03-11  本文已影响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) 注意

三. 原子操作类

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

上一篇下一篇

猜你喜欢

热点阅读