上一篇《Java锁手册》中以特性为角度对Java中的各类锁进行了初步的介绍,并且以平衡性能损耗和解决并发安全问题为核心思想,把锁的演进过程简单的梳理了一遍。所以对本文中出现的一些概念性问题如果不甚了解,可以回头再去看看上文。
作为一名开发者,光说不练假把式,如果说《Java锁手册》是文,那么这篇就是武,程序员玩的就是个文武双全咯。
CAS作为一个非阻塞式的无锁算法,其应用场景相当广泛,在java.util.concurrent.atomic包下面,所有原子类的实现都涉及CAS操作。各原子类的实现上,均依赖Java中的一个本地方法类——Unsafe,它的作用就是通过不同操作系统的系统指令完成CAS内存操作,下面看以下AtomicBoolean的实现:
package java.util.concurrent.atomic;
import sun.misc.Unsafe;
/**
* A {@code boolean} value that may be updated atomically. See the
* {@link java.util.concurrent.atomic} package specification for
* description of the properties of atomic variables. An
* {@code AtomicBoolean} is used in applications such as atomically
* updated flags, and cannot be used as a replacement for a
* {@link java.lang.Boolean}.
*
* @since 1.5
* @author Doug Lea
*/
public class AtomicBoolean implements java.io.Serializable {
private static final long serialVersionUID = 4654671469794556979L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicBoolean.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
……
}
这只是AtomicBoolean源码的声明部分,其信息量已经非常大了。首先注释中已经告诉我们CAS操作是通过Unsafe的compareAndSwapInt实现的:
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
其次,为什么说AtomicBoolean能够保证原子性,并发访问时能够实现对其他线程的可见性,就在于其value的声明为volatile:
private volatile int value;
接下来看看CAS方法的实现:
/**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
*
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful. False return indicates that
* the actual value was not equal to the expected value.
*/
public final boolean compareAndSet(boolean expect, boolean update) {
int e = expect ? 1 : 0;
int u = update ? 1 : 0;
return unsafe.compareAndSwapInt(this, valueOffset, e, u);
}
CAS算法在《Java锁手册》介绍过了,不细说。我们只需要知道,CAS过程的确是通过Unsafe的compareAndSwapInt实现的,因为这个方法是native的,懂C/C++的盆友自己深入挖掘,反正我不会,点到为止。
那么再说说如AtomicBoolean这类的原子类型有什么样的应用场景。就拿AtomicBoolean举例,我们常常在并发编程模式下,需要控制线程的执行逻辑,通过一个全局的标识位做为线程执行开关,此时就要求所有线程必须能够获取标识位的状态,那么AtomicBoolean就是一个非常好用的类型,简单示意代码如下:
public class Test {
/**
* 全局的开关标识位,控制所有线程仅一个能获得执行机会
*/
private static AtomicBoolean opened = new AtomicBoolean(false);
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
if (opened.compareAndSet(false, true)) {
// TODO 如果尚未开始则将标识位置为true,进入执行逻辑,否则不执行
}
}
});
}
因为标识位被任何一个线程修改状态之后,所有其他线程都会从主存获取到最新的状态值(可见性),所以能非常有效的控制线程执行权限。
由此也可以引申出,某些初始化过程仅允许执行一次的逻辑,也可以通过如上方式实现(包括各类资源的池的初始化,应用处理线程池、数据库连接池,还有各类资源、属性的配置过程等等)。
除了Java中大名鼎鼎的synchronized关键字,还有一个非常重要的锁实现——ReentrantLock,这个锁称之为重入锁,即允许已获取锁的线程直接进入嵌套的其他临界区。
ReentrantLock和synchronized一样,都是独占锁,但是它的应用场景更加广泛(但我相信没有多少开发者用过),原因在于它的实现方式,它不仅提供了独占特性,还支持公平和非公平两种模式可选(公平锁和非公平锁)。
看一下ReentrantLock的定义:
public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
/** Synchronizer providing all implementation mechanics */
private final Sync sync;
/**
* Base of synchronization control for this lock. Subclassed
* into fair and nonfair versions below. Uses AQS state to
* represent the number of holds on the lock.
*/
abstract static class Sync extends AbstractQueuedSynchronizer {
……
}
……
}
ReentrantLock实现了Lock接口,Lock接口的介绍大家自行百度下,我重点说明下ReentrantLock的其他特性。可以看到ReentrantLock内部持有一个Sync类型的内部类成员,并且该成员派生自AbstractQueuedSynchronizer(后文称AQS),这个成员就是实现加锁和释放锁的灵魂,并且Sync还有两个派生类,同样定义在ReentrantLock中:
/**
* Sync object for non-fair locks
*/
static final class NonfairSync extends Sync {...}
/**
* Sync object for fair locks
*/
static final class FairSync extends Sync {...}
其中NonfairSync实现了非公平锁特性,而FairSync则实现了公平锁特性,那么ReentrantLock如何控制采用那种特性模式呢?在构造函数里:
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
显而易见的,ReentrantLock默认采用非公平锁模式,只有显示的通过fair == true参数来构造,才可以启动公平锁模式。
非公平锁模式没有排队问题,所以实现相对简单些,我们再看一下NonfairSynch的主要实现逻辑:
/**
* Sync object for non-fair locks
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
需要注意咯,不同版本的JDK对以上的实现过程可能有所区别,至少我发现了两种不同的实现,我的JDK是1.8,所以只以我本机的源码解读,如果有朋友发现不一致,请查阅版本更新说明。
这段实现可以说非常简单了,加锁时首先通过compareAndSetState方法以CAS算法获取锁,一旦获取到了,则通过setExclusiveOwnerThread方法进行标记;否则通过acquire方法将当前线程加入一个阻塞队列,等待下次竞争,acquire方法实现如下:
/**
* Acquires in exclusive mode, ignoring interrupts. Implemented
* by invoking at least once {@link #tryAcquire},
* returning on success. Otherwise the thread is queued, possibly
* repeatedly blocking and unblocking, invoking {@link
* #tryAcquire} until success. This method can be used
* to implement method {@link Lock#lock}.
*
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquire} but is otherwise uninterpreted and
* can represent anything you like.
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
不再继续跟进了,回过头来再说说公平锁的实现有什么不同,只要提到公平,必然要排队,那么就一定会出现队列,所以看FairSync的实现是不是这样:
/**
* Sync object for fair locks
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
可以说毫无意外的,hasQueuePredecessors函数判断队列中是否有其他线程先于当前线程,如果有再看看是不是自己(重入哟,getExclusiveOwnerThread方法);否则的话,还是通过一次CAS来获取锁,然后进行标记,和非公平锁的执行过程一致。
那么ReentrantLock有什么应用场景呢?这里必须要和sycnhronized关键字做一个对比了。
首先sycnhronized关键字不支持中断,一旦死锁没有任何办法解决,而ReentrantLock是支持的,为什么能支持?原因就在于acquire方法:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
通过源码可以很清楚的看到,一旦锁竞争失败线程进入阻塞队列,并且这个阻塞队列支持中断,所以ReentrantLock就厉害了,这个特性synchronized做不到。
其次,因为ReentrantLock可选公平模式,这也是synchronized做不到的。
第三点,ReentrantLock还支持竞争锁的等待时间限制,通过tryLock方式实现:
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
两个重载函数,无参方法表示尝试加锁理解返回结果,而有参函数支持等待一段时间,并且支持中断。
那么诸位在项目实施的时候,就可以根据这些差异点,酌情选择使用哪种锁类型了。这里推荐大家两篇博文,一个是专门介绍ReentrantLock源码的,一个是介绍应用的:
不论是synchronized还是ReentrantLock都是独占锁,Java中提供了一种共享锁实现ReentrantReadWriteLock,而且实现也非常简单,看一下源码定义:
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {
private static final long serialVersionUID = -6992448646407690164L;
/** Inner class providing readlock */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** Inner class providing writelock */
private final ReentrantReadWriteLock.WriteLock writerLock;
/** Performs all synchronization mechanics */
final Sync sync;
......
}
当我看到Sync的时候无比亲切,这说明它也是支持公平和非公平两种模式的,不细说了。其他的不同在于两个私有成员ReadLock和WriteLock。
《Java锁手册》中已经说过,共享锁是支持其他线程获取锁的,只不过仅限于读操作,这个特性就是靠读写锁分离实现的。
这里源码就不再继续详细说明了,对源码的阅读方法可以参考上文,一法通万法通是我提倡的,反反复复的贴代码啥的意义不大,如果读者实在是懒得自己看,那么我推荐一篇博文专门写ReentrantReadWriteLock的Java并发编程–ReentrantReadWriteLock。
这里我更想说的是,共享锁其存在的意义。共享锁因为允许其他线程同时读资源,那么相对于独占锁来说就可以极大的提高并发性。如果存在大量读资源的应用场景,比如说频繁的缓存访问,那么共享锁的人格魅力就实实在在的体现出来了。
综上,其实各类锁实现都不复杂,复杂在于作为一名开发者,我们必须很清醒的知道当前应用场景的并发复杂性,并且根据并发特性来决定采用何种锁类型。
还是那句话,优秀的并发程序一定是能够在解决并发安全问题的同时,平衡系统资源消耗来提升性能的,千篇一律的synchronized不能给你的技能水平带来任何提高,开阔的视野可以,但它要求你了解更多的技能知识,我愿意付出这样的时间成本,因为它值。
如果想关注更多硬技能的分享,可以参考积少成多系列传送门,未来每一篇关于硬技能的分享都会在传送门中更新链接。