通过对SamePhore源码解读,了解双向链表及加锁原理

205 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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、初始化资源数

image.png

实现了一个非公平队列

image.png

image.png

可以看到设置了state

image.png

最终资源数,实际上就是给用volatile修饰的state变量

image.png

4-2、占用资源加锁

image.png

如果不传占用的资源数,则默认是一个

image.png

判断是否已经中断,如果未中断则进入tryAcquireShared

image.png

image.png

因为调用的是非公平锁,因此进入非公平锁的实现

image.png

image.png

4-3、判断是否有资源可以使用

image.png 这个地方通过自旋来获取锁资源。

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.

image.png

当有可用的资源的时候就加锁成功,那如果返回-1则代表加锁失败,就会走下面的方法,也就是当初始化的三个资源都耗尽之后,就会走下面的方法 image.png

4-4、任务加入队列,并再次尝试加锁

image.png

首先创建一个Node,看下addWaiter方法
final Node node = addWaiter(Node.SHARED);

如下首先创建了一个Node节点,传入当前线程,以及上一步创建的节点 image.png

获得尾节点
Node pred = tail;
//tail也是一个用volatile修饰的Node对象
private transient volatile Node tail;

假如现在是第四个线程进来,前三个线程还未释放锁,那么此时尾节点未被赋值,因此下面进入enq方法

image.png

4-4-1、队列双写链表的生成

进入enq方法之后会对Node的head和tail进行双向关联 image.png

因为首次资源耗尽,tail为null,则进入compareAndSetHead方法

image.png

通过CAS创建了了一个空节点,并设置为head节点,同时将head指向tail形成双向链表,以上步骤在流程图表示如下:

image.png

完成上面步骤之后,再次通过for自旋,将最开始创建的节点的上一个节点指向刚刚创建的空节点

image.png

流程图如下:

image.png

然后通过cas将节点设置为尾部节点

image.png 对应流程图如下:

image.png

如果加锁成功则,将空节点的next指向我们对应thred3节点,并返回空节点

image.png 对应流程图如下(到此形成了一个双向链表):

image.png

4-4-2、重新尝试加锁,如失败则阻塞当前线程

获取当前节点的前节点(也就是我们的空节点) image.png

image.png

获取到之后判断是否是head(此处肯定是head),则再次尝试加锁(目的是万一前面的线程执行完毕释放资源,这样就可以加锁成功,直接执行任务了),而现在肯定是加锁失败,则运行shouldParkAfterFailedAcquire(p, node)获取资源失败,然后进行阻塞操作

image.png 以上代码对应流程图如下:

image.png

那为什么要将头节点的waitState设置为-1呢,看下其注释,当状态为-1.则代表后续的线程需要唤醒执行unparking操作(简单的说就是阻塞队列中还有未完成的任务)

image.png

设置完头节点waitStatus=-1后返回false

image.png

for自旋继续接着工作

image.png

本次判断头节点,状态是否为-1,因为刚刚已设置,因此条件成立返回true image.png

则调用线程的park阻塞方法,如下:

image.png

加锁,并阻塞线程 image.png 到此步,流程图如下:

SemaPhore实现原理.jpg

4-5、已有阻塞队列,新线程再次尝试获取锁

上面讲述已有三个线程占用了所有资源,然后第四个线程进入到阻塞队列,下面看下第五个线程进入是如何处理的,依然还是获取资源

image.png

后面执行的流程和前面一样,直到addWaiter image.png

在未释放锁资源的情况下,第五个及第N的节点的操作过程 image.png

对应流程图如下:

SemaPhore实现原理 (1).jpg