本文自来我的公众号: https://mp.weixin.qq.com/s/JWoN10ydbVJKET_Xf8NagA
下面来了解下Doug Lea老大爷的sdk并发包java.util.concurrent,了解这些常用的实用工具类后可以加深我们对并发编程的印象。
SDK concurrent并发包图
下面分3块,讲解其中的某些工具类,以及这些类的产生过程。这里并不详解类的用法。
一. java.util.concurrent 容器类
java中容器主要分4大类:List、Set、Map、Queue。
我们经常使用的ArrayList,HashMap,其实它们不是线程安全的。如果要使用线程安全,怎么办呢?下面代码中,以ArrayList为例,
SafeArrayList内部持有一个ArrayList的实例list,所有访问list的方法都需要增加synchronized关键字,来保证原子性。
SafeArrayList<T>{
// 封装 ArrayList
List<T> c = new ArrayList<>();
synchronized
T get(int idx){
return c.get(idx);
}
synchronized
void add(int idx, T t) {
c.add(idx, t);
}
... ...
}
当然,我们写线程安全的集合时,不需要像上面代码那样写,因为java提供了线程安全的同步容器,如Vector,Hashtable。
但是,同步容器的缺点就是性能差,所有方法都用synchronized来保证互斥,串行度太高。
jdk1.5后,更高性能的容器诞生了,我们称之为:并发容器。
并发容器图
这里重点讲下 List容器- CopyOnWriteArrayList
CopyOnWriteArrayList,写的时候会将共享变量复制一份出来,那么读操作安全无锁了,保证了性能。
具体做法是:CopyOnWriteArrayList内部维护了一个数组,成员变量transient volatile Object[] array就指向这个内部数组,读操作都是基于array进行的;如果有写操作,CopyOnWriteArrayList会将array复制一份:Arrays.copyOf,写操作就在这个新数组上进行,执行完之后,再将array指向这个新的数组。
所以CopyOnWriteArrayList适用于读多写少的场景,并且可以容忍读写短暂的不一致。
二. java.util.concurrent.locks
SDK locks.* 图
1. ReentrantLock
ReentrantLock,中文翻译可重入锁,线程可以重复获取同一把锁,因为内层函数也可能需要锁,所以也叫递归锁。ReentrantLock构造函数ReentrantLock(boolean fair),可以用来构造公平锁,或者非公平锁。所谓公平锁,指的是等待时间越长越可能获取锁。
2. Lock&Condition
Java内置的管程,如synchronized 只支持一个条件变量,而locks中的Lock&Condition支持多个,这是重要的区别 。比如,要实现一个阻塞队列就需要2个条件变量 :队列不空 和 队列不满。另外,内置管程的实现函数有wait、notify、notifyAll。而locks.Condition中是await、signal、signalAll。
lock&condition在其他领域都有使用,比如异步转同步调用(异步都是用线程来实现的。)
tcp协议本身是异步的,发送完RPC请求后,线程不等待RPC的响应结果。我们工作中用到的RPC同步都是框架帮我们实现了异步转同步,具体做法是:当 RPC 返回结果之前,阻塞调用线程,让调用线程等待;当 RPC 返回结果后,唤醒调用线程,让调用线程重新执行。比如Dubbo,DefaultFuture用lock, 经典的等待(await)-通知机制(signal),实现了线程阻塞。
3. ReadWriteLock
ReadWriteLock 读写锁,适用于读多写少的场景,读写锁遵循三个基本原则:
a. 允许多个线程同时读共享变量;
b. 只允许一个线程写共享变量;
c. 如果正在执行一个写线程,禁止其他线程读操作。
正因为允许多个线程同时读,所以性能比互斥锁(synchronized)好。 并发库中ReetrantReadWriteLock实现了ReadWriteLock接口并添加了可重入的特性。ReentrantReadWriteLock支持锁的降级,但不支持锁的升级,即同一个线程中,在没有释放读锁的情况下,不能去申请写锁,否则会发生死锁。
4. StampedLock
stampedLock 支持三种锁:
a. 写锁
b. 悲观读锁
c. 乐观读,无锁机制, 最后用stamp验证有没有被写过,如果有,继续循环读直至stamp没有被修改,或者可升级成悲观读锁
可以看出,因为有乐观读,所以stampedLock比ReetrantReadWriteLock性能更高,但不支持重入;且写锁,悲观读锁不支持Condition变量。
stampedLock读模板:
final StampedLock sl = new StampedLock();
// 乐观读
long stamp = sl.tryOptimisticRead();
// 读入方法局部变量
......
// 校验 stamp
if (!sl.validate(stamp)){
// 升级为悲观读锁
stamp = sl.readLock();
try {
// 读入方法局部变量
.....
} finally {
// 释放悲观读锁
sl.unlockRead(stamp);
}
}
// 使用方法局部变量执行业务操作
......
stampedLock写模板:
long stamp = sl.writeLock();
try {
// 写共享变量
......
} finally {
sl.unlockWrite(stamp);
}
三. java.util.concurrent.atomic
SDK atomic.* 图
并发主要的问题其实是可见性、原子性和有序性的问题。可见性和有序性问题可以用volatile来解决,原子性问题可以采用互斥锁方案。那java sdk有没有提供简单的方案来解决原子性问题呢?
有,Java sdk提供了一系列的原子类,是一种无锁方案,完全没有加锁、解锁的性能消耗,同时还能保证互斥性。
那无锁方案的实现原理是什么呢? 很简单,硬件支持而已。CPU为了解决并发问题,提供了CAS指令,即Compare And Swap(Set)。作为一条 CPU 指令,CAS 指令本身是能够保证原子性的。
CAS 指令包含 3 个参数:共享变量的内存地址 A、用于比较的值(预期) B 和共享变量的新值 C;
并且只有当内存中地址 A 处的值等于 B 时,才能将内存中地址 A 处的值更新为新值 C。
所以,CAS 适合冲突较少的情况,如果太多线程在同时自旋,即A一直不等于B,那么长时间循环会导致 CPU 开销很大。
下面给出一段CAS使用的经典范例代码片段:
public final long getAndAddLong(
Object o, long offset, long delta){
long v;
do {
// 读取内存中的值
v = getLongVolatile(o, offset);
} while (!compareAndSwapLong(
o, offset, v, v + delta));
return v;
}
// 原子性地将变量更新为 x
// 条件是内存中的值等于 expected
// 更新成功则返回 true
native boolean compareAndSwapLong(
Object o, long offset,
long expected,
long x);
Java SDK 并发包里提供的原子类内容很丰富,分五个类别:基本数据类型、对象引用类型、数组、对象属性更新器和累加器。
原子类图
需要注意的是:对象引用类型中,对象引用的更新需要重点关注ABA问题,即A变成了B之后再变回A。解决方法是加版本号,类似于乐观锁。
例如,AtomicStampedReference实现的CAS方法就增加了版本号参数。
Java SDK提供的原子类大部分都实现了 compareAndSet() 方法,
但是原子类的方法都是针对一个共享变量的,使用场景有限,如果你需要解决多个变量的原子性问题,建议还是使用互斥锁方案。