Spring - JUC Semaphore 源码分析

前言
Github:https://github.com/yihonglei/jdk-source-code-reading(java-concurrent)

一 原理
Semaphore(信号量),内部维护一组许可证,通过 acquire 方法获取许可证,如果获取不到,则阻塞;

通过 release 释放许可,即添加许可证。许可证其实是 Semaphore 中维护的一个 volatile 整型 state 变量,

初始化的时候定义一个数量,获取时减少,释放时增加,一直都是在操作 state。

Semaphore 内部基于 AQS (同步器) 实现了公平或分公平两种方式获取资源。

Semaphore 主要用于限制线程数量、一些公共资源的访问。

下面通过实例体验 Semaphore 的含义,然后在从源码角度分析(jdk1.8)Semaphore 的实现原理。

二 实例
1、实例场景
公司每层楼都有一个卫生间,每个卫生间有5个大号坑!

卫生间是公共资源,这里用 Semaphore 来模拟现实的排队上厕所这件事情。

1)通过 acquire 获取锁

卫生间有 5 个坑,通过 acquire 来获取坑,获取到就用,如果没有获取到就阻塞,就憋着,排队等待,

总不能踹开门把人家拽出来吧,我们都是文明人。

2)通过 release 释放锁

上完厕所的人通过 release 释放坑,资源就让出来了,然后排队等待的人通过 acquire 尝试去获取资源上厕所,

获取不到的还是耐心等待。

3)Semphore 公平或非公平获取资源

这个例子举到这个地方,顺便把 Semphore 的公平或不公平获取资源分析下。

Semaphore 两个构造器:

public Semaphore(int permits) {
    sync = new NonfairSync(permits);
}
 
public Semaphore(int permits, boolean fair) {
    sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}

从代码可以清晰的看到,默认是非公平,fair 传 true 就是公平。

公平:就是一个人来了看到前面有人排队,就老老实实的跟着排。

非公平:就是一个人来了不看是否有排队,非得直接奔着坑去,拉一下门,发现都有人,然后老老实实的去队尾跟着排队。

区别:公平就是看到有人排队直接老实参与排队,非公平就是不看是否有排队,先去试一下,没资源再老老实实的排队。


2、实例代码

定义一个厕所类,里面通过Semaphore设置了5个坑:

package com.jpeony.concurrent.semaphore;
 
import java.util.concurrent.Semaphore;
 
/**
 * 卫生间有5个坑
 *
 * @author yihonglei
 */
public class Toilet {
    /**
     * 5个固定的茅坑
     */
    private static Semaphore semaphore = new Semaphore(5, true);
 
    /**
     * 茅坑
     */
    static class Pit {
        private String desc;
 
        public String getDesc() {
            return desc;
        }
 
        public void setDesc(String desc) {
            this.desc = desc;
        }
    }
 
    /**
     * 获取一个坑
     */
    public Pit getPit() throws InterruptedException {
        semaphore.acquire();
 
        Pit pit = new Pit();
        pit.setDesc("<<<获得坑了>>>");
        return pit;
    }
 
    /**
     * 释放一个坑
     */
    public Pit releasePit() {
        semaphore.release();
 
        Pit pit = new Pit();
        pit.setDesc("@@@释放了坑@@@");
        return pit;
    }
}

定义一个上厕所的线程,表示谁上厕所:

package com.jpeony.concurrent.semaphore;
 
/**
 * 大便!!!(画面感很强!)
 *
 * @author yihonglei
 */
public class ShiftThread extends Thread {
    private Toilet toilet;
    private Integer num;
 
    public ShiftThread(Toilet toilet, Integer num) {
        this.toilet = toilet;
        this.num = num;
    }
 
    @Override
    public void run() {
        try {
            // 获得坑
            Toilet.Pit pitAcquire = toilet.getPit();
            System.out.println("序号:" + num + ", " +pitAcquire.getDesc());
            // 解决大号
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 释放坑
            Toilet.Pit pitRelealse = toilet.releasePit();
            System.out.println("序号:" + num + ", " + pitRelealse.getDesc());
        }
    }
}

测试代码:

package com.jpeony.concurrent.semaphore;
 
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
/**
 * Semaphore(信号量)测试
 *
 * @author yihonglei
 */
public class SemaphoreTest {
    public static void main(String[] args) {
        try {
            Toilet toilet = new Toilet();
            ExecutorService executorService = Executors.newFixedThreadPool(10);
            for (int i = 1; i <= 30; i++) {
                executorService.execute(new ShiftThread(toilet, i));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

程序分析:

一开始 5 个坑都是空的,会有 5 个人先后获取了坑,其它的耐心等待,当某一个空出来的时候,

再按排队的顺序接着上,然后就是重复获取资源和释放资源的过程,但是最多只能同时有 5 个人在坑里。

这就是控制对公共资源的访问,因为很多资源是有限的,有限的资源就不能过度使用,否则就乱了,

你不能一个坑里蹲两人吧,如果有两个人,哪绝对不是在上厕所,而是在干别的,干啥就不知道了啊...
 

三 源码分析

1、构造器

1)Semaphore 构造器,permits 为传入的许可证数,默认非公平构造器;

2)Semaphore 构造器,permits 为传入的许可证数,fair 是 boolean 型的,如果传入 true,则公平,否则不公平;

默认使用的是非公平构造器。

NonfairSync 和 FairSync源码:

两者都继承了 Sync 同步器,初始化时都调用了父类构造器,同时都有一个获取信号的方法,稍后再分析获取信号的区别。

Sync 源码:

1)Sync 为 Semaphore 的内部静态类,同时继承了 AQS 同步器,主要是再获取或释放信号的时候通过

同步器的 CAS 算法实现原子更新。

2)构造器调用了 setState 方法,state 为 Semaphore 的一个成员变量,对应 setState 方法源码如下:
 

/**
 * The synchronization state.
 */
private volatile int state;
/**
 * Sets the value of synchronization state.
 * This operation has memory semantics of a {@code volatile} write.
 * @param newState the new state value
 */
protected final void setState(int newState) {
    state = newState;
}

所以从 Semaphore 构造器传进来的 permits 许可证数量,最后赋值到 volatile 变量 state。

volatile 是共享变量,内存可见,可用于线程间通讯,每个线程看到的 state,一定会拿到最新的 state 值。

Semaphore 获取信号或释放信号都是对 state 进行原子性减少或增加的操作。

 

2、acquire(获取信号量)

获取信号量默认方法源码:

acquire 有其它的重构方法,咱们这里分析默认获取信号量方法,其它的雷同。

获取信号量时,调用的是 Sync 的 acquireSharedInterruptibly 方法,默认参数为 1,

Sync 继承了 AQS,调用的其实是 AQS 的方法源码:

1)判断当前来获取信号量的线程是否中断,如果中断,直接跑线程中断异常。

2)tryAcauireShared 是真正去获取信号量的方法,获取到就返回当前信号量剩余数,也就是还有多少资源,否则就返回-1。

3)如果获取不到信号量,tryAcauireShared 方法返回-1,就会进入 doAcquireSharedInterruptibly 方法,

该方法会将哪些获取不到信号量的线程加入队列里面等待排队。

咱们继续看 tryAcauireShared 是如何处理信号量获取的,tryAcauireShared 方法签名:
 

该方法在 Semaphore 的静态内部类中有两个实现类:

先看公平的 FairSync:

 

1)先判断等待队列里面是否有正在等待获取信号的线程,如果有,直接就返回-1,外层代码会把该线程加入等待队列里面,

等待着获取信号量。就好比上厕所,你看到有人排队了,就不要去尝试获取资源了,老老实实排队就行了,厕所肯定是满位,

要不然别人也没傻到有位置不用,闲得没事在哪里排队玩。这就是公平获取信号的逻辑,看到有排队的,老老实实加入排队大军。

2)如果有资源,首先获取可用的资源,然后减掉我们想要获取的资源,得到剩余的资源,也就是 remaining。

判断条件 remaining<0 是防止虽然没有排队的,但是资源刚好占满了,这个时候来获取,必然没有资源,可用为 0,

remaining 就是负数,直接返回负数,外层会把该线程加入等待队列。

如果 remaining 是大于0的,则会执行后面的 compareAndSetState(available,remaining) 通过原子更新信号量方式

来获取信号了,如果更新成功,获取成功,返回 true,这个时候返回的 remaining 就是大于0的,并且这个玩意就是

剩余信号量。所以,当能获取信号量时,返回的int值就是当前剩余信号量。


然后再看非公平 NonfairSync 源码:

有没有发现,非公平相对于公平的代码只是去掉了关于等待队列的判断部分,非公平上来绝不判断

队列里面是否有等待获取信号的线程,而是直接获取资源,获取不到外层处才老老实实的加入等待队列。

就好比上厕所,大家都在排队呢,一个哥们来了,看到排队,不听人说没坑了,也不排队,就是奔着资源去,

然后挨个门拉一遍,发现都有人,才又老老实实的去排队。

小结下公平与非公平区别:

1)公平就是看到有线程等待获取信号了,就跟着排队,不去试着获取信号;

2)非公平就是无视排队,直接尝试获取信号量,获取不到再加入排队大军;


3、release(释放信号量)

释放信号量源码:

1)释放信号量。具体实现源码:

1)获取当前信号数量,也就是state变量的值。

2)当前信号量加上要释放的信号量等于释放后的信号量。

3)next<current 时抛异常,也就是释放后的信号量还不如释放前多,要不是传了负数值或者出现并发导致 state 为负数了。

4)通过 CAS 算法原子操作信号量 state,进行信号量释放,恢复信号量个数,也就是使 state 值增加 releases 个数。
 

2)如果 tryReleaseShared 尝试释放 state 成功,通过 doReleaseShared 进行后继节点唤醒具体处理

茅坑让出来了,等着的人就可以用了!等着的人也得保证先来后到,程序处理就麻烦些!

1)获取队列的头节点元素,如果不为null,并且不为尾节点,说白了,就是不止一个人等待,进入判断。

2)如果线程节点是需要唤醒的线程,则进行唤醒,获取资源使用。

3)失败后重试。

4)如果没有后继需要唤醒的节点,则退出,就相当于每人排队上厕所了,让出来资源就空着。

 

四 Semaphore 总结

1、Semaphore 内部维护一组信号量,即一个 volatile 的整型 state 变量。

2、Semaphore 分为公平或非公平两种方式,获取信号量或释放信号量的本质是对 state 进行原子的减少或增加操作。

3、获取不到信号的线程放在等待队列里面,释放信号的时候会唤醒后继节点。

4、Semaphore 主要用于对线程数量、公共资源(比如数据库连接池)等进行数量控制。
 

 


————————————————
版权声明:本文为CSDN博主「街灯下的小草」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/yhl_jxy/article/details/87279383

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值