Java基础-浅析解决并发的几种方式
前言
在上一篇中,我们讨论了Java中的关键字volatile
和synchronized
那么我们可以再想想,除了synchronized
我们还有什么解决并发的方式呢?
目录
目录除了我们目录里面,还有其他的解决并发的方式,如读写锁等,这里不作介绍。
一. 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
下面有很多种类型,比如AtomicBoolean
、AtomicLong
和AtomicReference
等,这里以 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) 注意
相比synchronized
和ReentrantLock
,乐观锁会减去线程挂起和恢复的开销,提升系统的运行效率。当然了,乐观锁也会有缺点,当线程的并发数量上来的时候,大量的线程操作相同的原子值,乐观更新的失败几率比较高,可能需要重复多次,因此,乐观锁适用于多读的系统。
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包括多个变量(加数),其总和为当前值。可以有多个线程更新不同的加数,线程个数增加时会自动提供新的加数。通常情况下,只有当所有工作都完成之后才需要总和的值,对于这种情况,这种方法会很高效。
因此,LongAdder
和LongAccumulator
适合高并发下的计数问题。
(3) 注意
看到这里,你可能会有这样的想法,XXXAdder
和XXXAccumulator
这么厉害了,是不是意味着我们可以抛弃AtomicXXX
了?当然不可能,XXXAdder
和XXXAccumulator
只适合高并发下的计数问题,除此之外,LongAccumulator
也使用了CAS
的方式处理数据。
四. 总结
总结总结得出来的使用技巧是:
- 优先考虑
synchronized
,需要性能调优的时候考虑Lock
。 - 优先使用传统的互斥方式,当性能方面的需求有明确指示的时候,考虑
Atomic
。 - 高并发计数优先考虑
XXXAdder
和XXXAccumulator
。
本人水平有限,难免会有错误,如有错误,欢迎提出。
Over~
引用
浅析LongAdder