JMM——AQS之Semaphore源码解读

Semaphore源码解读

1.Semaphore 是什么?

Semaphore 是一个的共享锁。共享锁就是允许多个线程同时获取锁,当然与锁的数量有关。如果把锁的数量设置成1,一定程度上也可以被称为独占锁😁。同样是基于AQS一些特征来实现的共享锁,支持公平与非公平特性。

2.Semaphore 的用法

用法一: 基础用法。线程每次获取一个锁。

// 创建对象并初始化,锁的数量,此处5代表,锁的数量是5,如果每次获取一个的话,同时允许5个线程通过
Semaphore semaphore = new Semaphore(5);
// 获取锁
semaphore.acquire();
// 释放锁
semaphore.release();

用法二: 基础用法。线程每次获取多个锁。

// 创建对象并初始化,锁的数量,此处5代表,锁的数量是6,如果每次获取一个的话,同时允许6个线程通过
Semaphore semaphore = new Semaphore(6);
// 获取锁,每次获取两个锁,即每个线程必须有两个锁,才能执行。也就是锁的数量是6,同时只能过去三个线程。
semaphore.acquire(2);
// 释放锁,当然每次获取多个锁,就会释放同样的锁,不然慢慢会死锁。
semaphore.release(2);

用法三: 基础用法。线程每次获取多个锁。

// 创建对象并初始化,锁的数量,此处5代表,锁的数量是5,如果每次获取一个的话,同时允许5个线程通过
Semaphore semaphore = new Semaphore(5);
// 尝试获取锁,如果获取不到,返回false,可以走其他逻辑。降级处理会使用到。
boolean b = semaphore.tryAcquire();
// 同样是释放锁
semaphore.release();

大致用法,分为这三种,当然还有一些其他的用法,研究一下API,即可使用。

3.Semaphore的API

在这里插入图片描述

  • Semaphore(int permits) ,Semaphore(int permits, boolean fair),构造方法,支持公平锁和非公平锁,需要指定锁的总数量。
  • acquire(),acquire(int permits),无返回值获取锁,可选参数permits 表示每次线程需要获取多少锁,才能被执行。默认值为1。允许线程被中断,抛出异常走异常的逻辑。
  • acquireUninterruptibly() ,acquireUninterruptibly(int permits),无返回值获取锁,可选参数permits 表示每次线程需要获取多少锁,才能被执行。默认值为1,不支持线程中断,即线程中断不做任何处理,还是按照原来的逻辑执行
  • tryAcquire(),tryAcquire(long timeout, TimeUnit unit),tryAcquire(int permits),tryAcquire(int permits, long timeout, TimeUnit unit),尝试获取锁,如果获取失败,返回false,获取成功返回true。参数:permits 同上,timeout 超时等待的时间,unit 时间单位。
  • release(int permits),释放锁,切记用多少锁,释放多少锁。不然会造成锁丢失。最终无锁可用,死锁。

4.Semaphore的源码解读(公平锁为例)

4.1 从构造方法入手

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
由此可知,最终是去设置同步器的state字段,state代表同步器的锁的数量。不明白可以看 初始AQS这边文章 至此,构造方法结束。

4.2 从semaphore.acquire(); 获取锁逻辑入手

在这里插入图片描述

// 参数arg=1
public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
   	// 首先对线程进行判断,如果此时线程被外部中断了,直接抛出异常,外部获取异常,可以走异常逻辑。
   	if (Thread.interrupted())
         throw new InterruptedException();
    // 没有话,就尝试去获取锁
    if (tryAcquireShared(arg) < 0)
    // 当有线程获取锁失败,走入队逻辑。
   doAcquireSharedInterruptibly(arg);
}

首先回去进行线程的中断信号判断,有中断信号直接抛出异常,外部接收异常就可以做其他的处理。
其次,会走 tryAcquireShared(arg),之后走doAcquireSharedInterruptibly()方法
图解:
在这里插入图片描述

4.2.1 查看tryAcquireShared();方法

在这里插入图片描述
查看公平锁的逻辑
在这里插入图片描述

// 参数acquires等于1,此处就是公平锁的尝试获取锁的逻辑
protected int tryAcquireShared(int acquires) {
			// 循环加CAS,典型的自旋锁。直到锁被释放完,就会结束此方法。
            for (;;) {
            	// 因为是公平锁,先尝试查看队列中是否有节点。
                if (hasQueuedPredecessors())
                	// 有的话,说明锁已经被获取完了,直接返回-1。为什么返回-1,-1就代表锁没了,思考思考哦
                    return -1;
                    // 获取state状态,即锁的数量,就是构造方法放入的。
                int available = getState();
                // 锁数量-1,表示有一个线程获取锁了。
                int remaining = available - acquires;
                // 当第一个条件成功为true的时候就会返回remaining,所以state不可能为-1。用到了短路或
                if (remaining < 0 ||
                // compareAndSetState 保证的是state安全性,与锁的数量无关。
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }

图解:
在这里插入图片描述

4.2.2 查看doAcquireSharedInterruptibly();方法

在这里插入图片描述

// 参数arg=1
private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        // 直接入队,入队后返回当前节点。入队逻辑之前讲过。可以参考之前的博客。当前节点的mode是共享。
        final Node node = addWaiter(Node.SHARED);
        // 失败的条件判断
        boolean failed = true;
        try {
        	// 自选锁
            for (;;) {
            	// 获取当前节点的前置节点。
                final Node p = node.predecessor();
                // 如果等于头节点
                if (p == head) {
                	// 再次尝试获取锁,
                    int r = tryAcquireShared(arg);
                    // 返回值大于等于0,表示已经获取锁
                    if (r >= 0) {
                    	// 设置头和传播,看下面代码
                        setHeadAndPropagate(node, r);
                        // 把头部节点丢弃
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                // 如果获取不到锁。走挂此逻辑。主要是将前一个节点是成SIGNAL表示后续节点需要被唤醒。
                if (shouldParkAfterFailedAcquire(p, node) &&
                	// 挂起当前线程,此时阻塞完成。
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
// 参数node为当前节点,propagate为当前剩下锁的数量。
private void setHeadAndPropagate(Node node, int propagate) {
		// 因为此阶段,只会有一个节点获取锁,所以不存在安全问题。
        Node h = head; 
        // 把当前的节点,设置成头部节点,
        setHead(node);
        // 当前剩下锁的数量大于0,且头部节点等于null,此处使用的短路或,第一条件成立的时候,就会执行下面逻辑,剩下的判断,健壮性判断,无需多考虑
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
           	// 获取当前节点后置节点。
            Node s = node.next;
            // 如果当前节点等于空,或者当前节点是共享节点。
            if (s == null || s.isShared())
            	// 走这里此处是唤醒线程,后面来讲。
                doReleaseShared();
        }
    }

图解
在这里插入图片描述

4.3 从semaphore.release(); 释放锁逻辑入手

在这里插入图片描述
解锁逻辑不分公平和非公平锁
在这里插入图片描述

4.3.1 tryReleaseShared();
// 参数releases等于1
protected final boolean tryReleaseShared(int releases) {
			// 自选锁,
            for (;;) {
            	// 首先获取当前的同步器的状态,表示锁的数量
                int current = getState();
                // 锁的数量+1
                int next = current + releases;
                // 健壮判断
                if (next < current) // overflow
                    throw new Error("Maximum permit count exceeded");
                // cas 设置锁的数量
                if (compareAndSetState(current, next))
                    return true;
            }
        }

图解
在这里插入图片描述

4.3.2 doReleaseShared();唤醒线程
// 释放共享锁
private void doReleaseShared() {
		// 此处是自选锁。
        for (;;) {
        	// 获取头部节点,每次过来都会获取当前的头部节点。
            Node h = head;
            // 头部节点不等于null,且头部节点不等于尾部节点。因为AQS是根据节点的waitStatus判断后置节点的。
            if (h != null && h != tail) {
            	// 获取头部的waitStatus
                int ws = h.waitStatus;
                // 如果头部节点的waitStatus等于Node.SIGNAL,说明后置节点需要被唤醒。
                if (ws == Node.SIGNAL) {
                	// 如果设置失败,接着设置,一般不会 
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;  
                    // 唤醒线程,唤醒的是头部节点的后置节点的线程,回到java.util.concurrent.locks.AbstractQueuedSynchronizer#parkAndCheckInterrupt 方法
                    unparkSuccessor(h);
                }
                // 如果头部节点不是Node.SIGNAL,说明后置节点不需要被唤醒,尝试设置成传播。
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            // 当前头部节点没有变化,且已经唤醒成功,结束循环。
            if (h == head)                   // loop if head changed
                break;
        }
    }

图解:
在这里插入图片描述

4.Node.PROPAGATE 字段解读

在这里插入图片描述
Node.PROPAGATE是为了解决一个bug,后来新增的。找到一片博客。写的还不错。拷贝过来参考一下。
而可能出现bug的测试代码如下:

 1  import java.util.concurrent.Semaphore;
 2
 3  public class TestSemaphore {
 4
 5    private static Semaphore sem = new Semaphore(0);
 6
 7    private static class Thread1 extends Thread {
 8        @Override
 9        public void run() {
10            sem.acquireUninterruptibly();
11        }
12    }
13
14    private static class Thread2 extends Thread {
15        @Override
16        public void run() {
17            sem.release();
18        }
19    }
20
21    public static void main(String[] args) throws InterruptedException {
22        for (int i = 0; i < 10000000; i++) {
23            Thread t1 = new Thread1();
24            Thread t2 = new Thread1();
25            Thread t3 = new Thread2();
26            Thread t4 = new Thread2();
27            t1.start();
28            t2.start();
29            t3.start();
30            t4.start();
31            t1.join();
32            t2.join();
33            t3.join();
34            t4.join();
35            System.out.println(i);
36        }
37    }
38  }

其实上面所做的操作无非就是创建了四个线程:t1和t2用于获取信号量,而t3和t4用于释放信号量,其中的10000000次for循环是为了放大出现bug的几率,join操作是为了阻塞主线程。现在就可以说出出现bug的现象了:也就是这里可能会出现线程被hang住的情况发生(遗憾的是,我并没有模拟出来这个bug)。

可以想象这样一种场景:假如说当前CLH队列中有一个空节点和两个被阻塞的节点(t1和t2想要获取信号量但获取不到被阻塞在CLH队列中(state初始为0)):head->t1->t2(tail)。

时刻1:t3调用release->releaseShared->tryReleaseShared,将state+1变为1,同时发现此时的head节点不为null并且waitStatus为-1,于是继续调用unparkSuccessor方法,在该方法中会将head的waitStatus改为0;
时刻2:t1被上面t3调用的unparkSuccessor方法所唤醒,调用了tryAcquireShared,将state-1又变为了0。注意,此时还没有调用接下来的setHeadAndPropagate方法;
时刻3:t4调用release->releaseShared->tryReleaseShared,将state+1变为1,同时发现此时的head节点虽然不为null,但是waitStatus为0,所以就不会执行unparkSuccessor方法;
时刻4:t1执行setHeadAndPropagate->setHead,将头节点置为自己。但在此时propagate也就是剩余的state已经为0了(propagate是在时刻2时通过传参的方式传进来的,那个时候-1后剩余的state是0),所以也不会执行unparkSuccessor方法。
至此可以发现一轮循环走完后,CLH队列中的t2线程永远不会被唤醒,主线程也就永远处在阻塞中,这里也就出现了bug。那么来看一下现在的AQS代码在引入了PROPAGATE状态后,在面对同样的场景下是如何解决这个bug的:

时刻1:t3调用release->releaseShared->tryReleaseShared,将state+1变为1,继续调用doReleaseShared方法,将head的waitStatus改为0,同时调用unparkSuccessor方法;
时刻2:t1被上面t3调用的unparkSuccessor方法所唤醒,调用了tryAcquireShared,将state-1又变为了0。注意,此时还没有调用接下来的setHeadAndPropagate方法;
时刻3:t4调用release->releaseShared->tryReleaseShared,将state+1变为1,同时继续调用doReleaseShared方法,此时会将head的waitStatus改为PROPAGATE;
时刻4:t1执行setHeadAndPropagate->setHead,将新的head节点置为自己。虽然此时propagate依旧是0,但是“h.waitStatus < 0”这个条件是满足的(h现在是PROPAGATE状态),同时下一个节点也就是t2也是共享节点,所以会执行doReleaseShared方法,将新的head节点(t1)的waitStatus改为0,同时调用unparkSuccessor方法,此时也就会唤醒t2了。
至此就可以看出,在引入了PROPAGATE状态后,可以有效避免在高并发场景下可能出现的、线程没有被成功唤醒的情况出现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值