阻塞式锁,在同一时刻只能有一个线程在执行,当一个线程执行完成后,再去释放下一个线程,而共享式是指,锁是可以被共享的,表现形式为,在同一时刻可以有多个线程运行。
CountDownLatch、Semaphore都属于共享锁。基于网上有好多博客都是分析CountDownLatch,所以我在此处分析一下Semaphore。
public class SemaphoreTest {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
semaphore.acquire();//获取锁
System.out.println(Thread.currentThread().getName() + " " + new Date() + " 我执行了");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release();//释放锁
}
}
}).start();
}
}
}
上面的代码很简单,初始化一个可容纳3个线程的信号量,初始化10个线程,每个线程执行一次打印(延迟1秒)。
以上代码运行结果:
和排它锁相比,同一时刻允许多个线程同时执行。
看下Semaphore的内部类结构
从类结构上可以看出,Semaphore也是支持公平锁和非公平锁的实现,其锁的实现同样是通过Sync集成AQS实现。
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
acquire sync通过调用AQS的acquireSharedInterruptibly方法实现
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())//首先判断线程是否被终端
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)//尝试获取锁,获取不到则执行下面方法,小于0代表获取不到锁,大于0代表获取成功,0代表成功,但没有剩余可用资源
doAcquireSharedInterruptibly(arg);
}
cquireSharedInterruptibly名字可以看出这个方法是可相应中断的共享式获取锁。
tryAcquireShared肯定也是一个空方法,留给子类实现。
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
看下Semaphore 公平锁的tryAcquireShared实现
protected int tryAcquireShared(int acquires) {
for (;;) {//CAS的方式循环获取锁
if (hasQueuedPredecessors())//队列中有线程在等待,直接返回-1,代表当前没有可用资源
return -1;
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))//要么没有可用资源,要么当前资源CAS操作成功,则返回可用的资源数
return remaining;
}
}
当没有可用的资源时,执行doAcquireSharedInterruptibly方法
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED);//保证节点一定会被插入到队尾。
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())//操作流程和排它锁一样
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
此时的数据结构如下:
主要看下setHeadAndPropagate方法,这个方法是在线程被唤醒之后要执行的
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
/*
* Try to signal next queued node if:
* Propagation was indicated by caller,
* or was recorded (as h.waitStatus either before
* or after setHead) by a previous operation
* (note: this uses sign-check of waitStatus because
* PROPAGATE status may transition to SIGNAL.)
* and
* The next node is waiting in shared mode,
* or we don't know, because it appears null
*
* The conservatism in both of these checks may cause
* unnecessary wake-ups, but only when there are multiple
* racing acquires/releases, so most need signals now or soon
* anyway.
*/
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())//节点为空(下面没有等待的线程)或者下一个节点依然是共享模式,那么就执从头结点开始执行唤醒
doReleaseShared();
}
}
这种唤醒方式和排它锁的区别为,共享模式,每一次都是从头结点开始,唤醒一个线程,紧接着就会去试图唤醒下一个节点的线程,只要有足够的资源,线程就会被唤醒,就这样循环往复。而不像排它锁一样,要等待一个节点执行完成,再去唤醒下一个节点。
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {//保证锁一定被释放
doReleaseShared();//开启释放流程
return true;
}
return false;
}