Java并发编程之显式锁机制详解
|
我们之前介绍过synchronized关键字实现程序的原子性操作,它的内部也是一种加锁和解锁机制,是一种声明式的编程方式,我们只需要对方法或者代码块进行声明,Java内部帮我们在调用方法之前和结束时加锁和解锁。而我们本篇将要介绍的显式锁是一种手动式的实现方式,程序员控制锁的具体实现,虽然现在越来越趋向于使用synchronized直接实现原子操作,但是了解了Lock接口的具体实现机制将有助于我们对synchronized的使用。本文主要涉及以下一些内容:
一、接口Lock的基本组成成员 Lock 位于java.util.concurrent.locks包下,源码如下:
public interface Lock {
void lock();
void lockInterruptibly()
boolean tryLock();
boolean tryLock(long time,TimeUnit unit)
void unlock();
Condition newCondition();
}
其中, void lock();:调用该方法将获得一个锁的入口 二、可重入锁ReentrantLock的基本使用 ReentrantLock是接口 Lock的一个最主要的实现类,不仅实现了Lock中的基本的加锁释放锁的方法,还扩展了自己的方法。它有两个构造方法:
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
参数 fair用于保证锁机制的公平策略,公平的策略会是的等待时间越长的线程优先获得锁。保证公平必然会降低性能,所以ReentrantLock默认并不保证公平。我们用ReentrantLock来实现对程序的原子操作:
public class MyThread extends Thread{
private static Lock lock = new ReentrantLock();
public static int count;
@Override
public void run() {
try {
Thread.sleep((int)Math.random()*100);
lock.lock();
count++;
lock.unlock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
当我们在主程序中启动一百个线程随机唤醒对count进行加一时,无论运行多少次,结果都是一百,也就是说我们的ReentrantLock是可以为我们保证原子操作的。 ReentrantLock还有一个特性就是可以重入性,即在本身获得某个锁的前提下可以随意进入被该锁锁住的其他方法,对于一个锁可以重复进入。除此之外,ReentrantLock还具有一些其他的有关锁信息的方法:
三、深入ReentrantLock的实现原理 ReentrantLock依赖CAS和LockSupport来实现,LockSupport有点像工具类,它主要提供两类方法,park和unpark。
调用park方法会使得当前线程丢失CPU使用权,从Runnable状态转变为Waiting状态。而unpark方法则反过来让Waiting状态的某个线程转变状态为Runnable,等待操作系统调度。parkNanos和parkUntil是和时间相关的两个park的变种,parkNanos指定线程要等待的时间,parkUntil则指定线程要等待到什么时候,这个时间是一个绝对时间,相对于纪元的毫秒数。 Java的并发包中有很多并发工具,ReentrantReadWriteLock,Semaphore,CountDownLatch,ReentrantLock等。这些工具有很多的共同特性,于是Java为我们抽象了一个类AbstractQueuedSynchronizer(AQS)来表示这些工具的共性。ReentrantLock是其的一个实现类,内部有三个内部类:
abstract static class Sync extends AbstractQueuedSynchronizer{
//......
}
static final class NonfairSync extends Sync{
//...........
}
static final class FairSync extends Sync {
//.............
}
Sync 继承了AQS并对其中的大部分代码进行了简单的实现,FairSync 和NonfairSync 是针对公平策略而定义的,如果构造ReentrantLock的时候指定公平的策略,那么其内部的所有方法都依赖这个FairSync ,否则就全部依赖NonfairSync。接着看ReentrantLock的构造函数:
private final Sync sync;
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
两个构造方法最终会对sync进行初始化,而sync的将在后续的方法中起到相当大的作用。我们先看lock方法的具体实现:
public void lock() {
sync.lock();
}
ReentrantLock的lock方法调用的sync的lock方法,而在sync中的lock方法是一个抽象的方法,也就是说这个方法的具体实现在子类中,我们看NonfairSync中的实现:
final void lock() {
if (compareAndSetState(0,1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
AQS中有一个整型类型的State变量,它用于标识当前锁被持有的次数,该值为0表示当前锁没有被任何线程持有。compareAndSetState是AQS中的方法,该方法调用了unsafe.compareAndSwapInt方法以CAS方式对State进行了更新,如果state的值为0,说明该锁并没有被任何线程持有,那么当前线程将持有该锁并将state的值赋为1。 这就完成了获取的动作,一旦后续的线程尝试访问临界区代码,在前面的线程没有释放锁之前,将会调用 acquire(1)。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE),arg))
selfInterrupt();
}
tryAcquire还是调用了AQS中的实现,
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0,acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
第一个if判断,想要持有的锁是否被持有(虽然之前判断过了,但是有可能在我们调用nonfairTryAcquire方法的期间,之前的线程释放了该锁),如果未被任何线程持有,那么将直接持有该锁。 第二个if判断,如果当前锁的持有者就是当前线程,表示这是同线程的重入操作,于是增加锁定次数并设置state的值。 整个方法结束之后,如果当前线程获得了锁,都将返回true,否则都会返回false。而如果tryAcquire方法返回true,那么整个acquire方法也将结束,否则就说明当前线程并没有通过锁,需要被阻塞。那么就会调用acquireQueued(addWaiter(Node.EXCLUSIVE),arg)方法。
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(),mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred,node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
addWaiter方法将当前线程包裹成一个Node结点,添加到AQS内部所维护的一个等待队列并返回该Node结点。最后调用acquireQueued方法:
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);
}
}
该方法首先会去获得node的前一个结点,判断如果是head结点,那么说明当前的node结点是整个等待队列上的第一个等待的结点。于是让它尝试着去获得锁,如果能够获得锁,将从等待队列中清除它并返回。 如果发现当前结点前面还有等待的结点或者尝试获取锁失败,那么将会调用shouldParkAfterFailedAcquire方法判断该结点锁对应的线程是否需要被unpark阻塞,并最终调用LockSupport.park(this)阻塞当前线程。 在第一个线程持有该锁的前提下,成功阻塞了第二个线程。这大概就是整个lock方法的调用链流程。 接下来看看unlock的具体实现,
public void unlock() {
sync.release(1);
}
(编辑:安卓应用网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |
