在Java 1.5之后,并发包中新增了Lock接口用来实现锁功能,它提供了Synchronized关键字类似的功能,只是在使用时需要显式地获取锁和释放锁。虽然它缺少了隐式获取锁释放锁的便捷性,但是却拥有了锁释放和获取的可操作性、可中断地获取锁以及超时获取锁等多种选择。
1 Lock接口
Lock接口的主要api如下:
1)void lock():获取锁,调用该方法的当前线程或获取锁,并从该方法返回,没有获取锁的将会自旋等待。
2)void lockInterruptibly () throws InterruptedException:可中断的获取锁,该方法会响应中断,即在获取锁的自旋等待中,可以中断当前线程。
3)boolean tryLock():尝试非阻塞的获取锁,调用该方法会立刻返回,获取则返回true。
4)Condition newCondition():返回等待通知组件,类似于Object.wait与Object.notify方法,可以使线程进入waiting状态。
5)相对应的unlock()方法。
在Lock的具体实现中,基本都是通过聚合了一个队列同步器(AbstractQueuedSynchronizer,AQS)的子类来完成线程访问控制的。下面我们将详细介绍AQS的使用和实现。
2 队列同步器AQS
2.1 同步器的接口
上述的几个方法将被模板方法引用,我们在实现自定义方法时,将调用这些模板方法。模板方法如下:
1)void acquire(ing arg):独占式获取同步锁(调用tryAcquire()),成功的话,则同步器的锁被该线程拥有(同步器记录下该线程),方法返回。否则进入同步队列等待。
2)void acquireInterruptibly(int arg):与1)相同,但是能响应中断,在同步队列等待时,可以响应中断,抛出异常并返回。
3)boolean AcquireShared(int arg):共享式的获取锁。
4)Collection<Thread> getQueuedThreads():获取等待在同步队列上的线程。
5)其他独占、共享、中断的组合获取锁和释放锁的方法。
2.2 模板方法的实现
1.同步队列
2.独占式同步状态获取与释放
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
public boolean tryAcquire(int acquires){
if(this.compareAndSetState(0, 1)){ //如果锁未被占用,则设置为1
this.setExclusiveOwnerThread(Thread.currentThread()); //将此线程设为锁的拥有者
return true;
}
return false;
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
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;
}
可以看到,当状态为0时,会同上面的不可重入锁一样进行加锁并取得锁。但如果不为0,则对线程进行判断,如果当前线程是拥有锁的线程,则可以正确返回,执行同步方法。否则,将加入同步队列等待。
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()) //会调用LockSupport.park()方法阻塞自己,等待前继节点唤醒自己
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
自旋获取锁的过程:
判断当前节点的前驱节点。
需要获取当前节点的前驱节点的状态,当前驱节点是头结点并且当前节点(线程)能够获取状态(tryAcquire方法成功),
代表该当前节点占有锁,如果满足上述条件,那么代表能够占有锁,根据节点对锁占有的含义,设置头结点为当前节点(setHead)。
如果没有满足上述条件,判断前一个节点的状态,并调用parkAndCheckInterrupt方法使得当前线程阻塞,直到unpark调用(前一个节点释放锁的时候会通知后继节点),Thread的interrupt调用,然后重新轮训去尝试上述操作。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
3 共享式同步状态的获取与释放
共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。其获取锁的代码如下:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
在acquireShared()放法中,先会tryAcquireShared(arg),如果大于0成功,则获取同步状态。否则,加入同步队列,阻塞,等待前继节点执行完同步方法后唤醒,重新尝试获取同步状态。共享锁的释放同独占锁,需要唤醒后继节点。
4 独占式超时获取同步状态
可以在指定时间内获取同步状态,如果获取成功,则返回true,否则,返回false。在acquireSharedInterruptibly()的基础是增加了时间限制。不再详述。