持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第19天,点击查看活动详情
上篇文章叙述了SamePhore信号量锁的功能,那其底层是如何实现的呢,本篇文章将进行叙述。
主要针对如下代码进行跟踪
public class SemaphoreTest {
public static void main(String[] args) {
// 声明3个窗口 state: 资源数
Semaphore windows = new Semaphore(3);
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
// 占用窗口 加锁
windows.acquire();
System.out.println(Thread.currentThread().getName() + ": 开始买票");
//模拟买票流程
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName() + ": 购票成功");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放窗口
windows.release();
}
}
}).start();
}
}
}
四、实现源码解读
下面来看一下Semaphore的实现原理
4-1、初始化资源数
实现了一个非公平队列
可以看到设置了state
最终资源数,实际上就是给用volatile修饰的state变量
4-2、占用资源加锁
如果不传占用的资源数,则默认是一个
判断是否已经中断,如果未中断则进入tryAcquireShared
因为调用的是非公平锁,因此进入非公平锁的实现
4-3、判断是否有资源可以使用
这个地方通过自旋来获取锁资源。
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
//获得当前资源,初始化的时候设置了3
int available = getState();
//用初始化资源-本次请求的资源,获得可使用资源
int remaining = available - acquires;
//当还有剩余资源的时候,会进入compareAndSetState(available, remaining);
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
解析compareAndSetState(available, remaining)的作用,分别传入了当前可用资源,以及剩余资源数
可以看到使用了CAS实现了一个加锁的操作->这样如果有可用资源就可以加锁成功,加锁成功上面的nonfairTryAcquireShared方法就会返回当前剩余可用的资源,如果加锁不成功,那就再次通过for自旋尝试加锁。如果没有资源则返回-1.
当有可用的资源的时候就加锁成功,那如果返回-1则代表加锁失败,就会走下面的方法,也就是当初始化的三个资源都耗尽之后,就会走下面的方法
4-4、任务加入队列,并再次尝试加锁
首先创建一个Node,看下addWaiter方法
final Node node = addWaiter(Node.SHARED);
如下首先创建了一个Node节点,传入当前线程,以及上一步创建的节点
获得尾节点
Node pred = tail;
//tail也是一个用volatile修饰的Node对象
private transient volatile Node tail;
假如现在是第四个线程进来,前三个线程还未释放锁,那么此时尾节点未被赋值,因此下面进入enq方法
4-4-1、队列双写链表的生成
进入enq方法之后会对Node的head和tail进行双向关联
因为首次资源耗尽,tail为null,则进入compareAndSetHead方法
通过CAS创建了了一个空节点,并设置为head节点,同时将head指向tail形成双向链表,以上步骤在流程图表示如下:
完成上面步骤之后,再次通过for自旋,将最开始创建的节点的上一个节点指向刚刚创建的空节点
流程图如下:
然后通过cas将节点设置为尾部节点
对应流程图如下:
如果加锁成功则,将空节点的next指向我们对应thred3节点,并返回空节点
对应流程图如下(到此形成了一个双向链表):
4-4-2、重新尝试加锁,如失败则阻塞当前线程
获取当前节点的前节点(也就是我们的空节点)
获取到之后判断是否是head(此处肯定是head),则再次尝试加锁(目的是万一前面的线程执行完毕释放资源,这样就可以加锁成功,直接执行任务了),而现在肯定是加锁失败,则运行shouldParkAfterFailedAcquire(p, node)获取资源失败,然后进行阻塞操作
以上代码对应流程图如下:
那为什么要将头节点的waitState设置为-1呢,看下其注释,当状态为-1.则代表后续的线程需要唤醒执行unparking操作(简单的说就是阻塞队列中还有未完成的任务)
设置完头节点waitStatus=-1后返回false
for自旋继续接着工作
本次判断头节点,状态是否为-1,因为刚刚已设置,因此条件成立返回true
则调用线程的park阻塞方法,如下:
加锁,并阻塞线程
到此步,流程图如下:
4-5、已有阻塞队列,新线程再次尝试获取锁
上面讲述已有三个线程占用了所有资源,然后第四个线程进入到阻塞队列,下面看下第五个线程进入是如何处理的,依然还是获取资源
后面执行的流程和前面一样,直到addWaiter
在未释放锁资源的情况下,第五个及第N的节点的操作过程
对应流程图如下: