JUC源码系列之Semaphore源码解析

目录

  • Semaphore 简介
  • Semaphore 使用示例
  • Semaphore 实现原理
  • Semaphore 源码解析

Semaphore 简介

Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。

使用示例

在一个需求,开启 30 个线程并发读取文件,并存储至数据库,但数据库只有 10 个连接,如果 30 个线程同时存库,则会导致无法获取数据库连接,这时必须控制只有 10 个线程同时获取数据库连接进行数据保存。

public class Main {

    private static final int THREAD_COUNT = 30;
    private static final int CONNECTION_COUNT = 10;

    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(CONNECTION_COUNT);
        IntStream.range(0, THREAD_COUNT).forEach(i -> new Thread(() -> {
            try {
                semaphore.acquire();
                System.out.println("Thread["+i+"] execute save");
                Thread.sleep(1000L);
                semaphore.release();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start());
    }
}

虽然有 30 个线程在运行,但是只允许 10 个并发执行,Semaphore 在构建时传入初始值,表示可用的许可证数量,线程调用 acquire 方法可以获取一个许可证,当 10 个线程调用 acquire 方法,许可证被领完,后续线程则会阻塞,领取许可证的线程执行 release 方法归还许可证,此时被阻塞的线程就可以继续获取许可证。

Semaphore 还提供了一些其它方法

  • int availablePermits():返回信号量中当前可用的许可证数量
  • int getQueueLength():返回正在等待获取许可证的线程数
  • boolean hasQueuedThreads():是否有线程正在等待获取许可证
  • void reducePermits(int reduction):减少 reduction 个许可证
  • Collection getQueuedThreads():返回所有等待获取许可证的线程集合

实现原理

Semaphore 内部是通过 AQS 同步器来实现的,多个线程同时运行,可以使用 AQS 的共享锁机制,控制并发线程数量,可以通过共享状态值进行控制。

  • 第一步:初始化可颁发许可证数量,后台处理为:创建 Semaphore 对象,将 AQS 同步状态设置为可颁发许可证数量。
  • 第二步:颁发许可证,多个线程调用 acquire() 方法领取许可证,后台处理为:执行加共享锁,每次加锁同步状态减 1,如果同步状态大于等于 0,加锁成功,即成功颁发许可证,如果同步状态小于 0,则加锁失败,线程进入 AQS 等待队列阻塞等待,所以同步状态最小为 0。
  • 第三步:归还许可证,获取许可证的线程执行完逻辑后需要调用 release() 方法归还许可证,后台处理为:执行释放共享锁,释放成功,同步状态加 1,同时会唤醒 AQS 等待队列中的队首线程。

Semaphore 提供了公平锁与非公平锁两种模式,所谓公平与非公平指的是领取许可证时,需要判断等待队列中是否有线程在等待,如果有线程在等待,那么公平锁会直接进入等待队列队尾,而非公平锁则与等待队列中的线程竞争这个许可证。通俗点讲就是,有一队人在排队挂号,非公平锁机制就是,先去窗口与第一个人挤一下,看能不能插个队,如果插不上队,就去后面排队,而公平锁机制就是老实人机制,直接去后面排队。虽然生活中做个老实人比较好一点,但在计算机中,更鼓励不要太老实。

公平锁与非公平锁的机制 ReentrantLock 也有提供,可以参考本系列的 JUC源码系列之ReentrantLock源码解析

源码解析

通过上面的实现原理分析,已经大概了解了基本实现思路,以非公平锁为例,看一下内部实现代码。

首先查看构造方法,Semaphore 有两个构造方法,permits 参数用于设置可颁发许可证数量,fair 参数用于设置内部使用公平锁还是非公平锁,默认使用非公平锁。


public class Semaphore implements java.io.Serializable {
    private final Sync sync;
    // 默认创建非公平锁
    public Semaphore(int permits) {
        sync = new NonfairSync(permits);
    }
    // 根据入参决定创建公平锁还是非公平锁
    public Semaphore(int permits, boolean fair) {
        sync = fair ? new FairSync(permits) : new NonfairSync(permits);
    }
}

以非公平锁为例,继续查看源码,公平锁与非公平锁的差别无非是加锁时的操作有点差异,在文章后面会解释其间差异。

非公平锁的实现非常简单,只有两个方法,构造方法直接调用父类的构造方法,tryAcquireShared 方法执行加锁时会被调用,也是执行父类中的已有实现,所以主体实现其实都在 Sync 类中。

static final class NonfairSync extends Sync {

    NonfairSync(int permits) {
        super(permits);
    }

    protected int tryAcquireShared(int acquires) {
        return nonfairTryAcquireShared(acquires);
    }
}

Sync 类继承了 AQS 同步器,现在看的是构造方法,构造方法直接调用 AQS 提供的 setState 方法,设置同步状态的值,将其设置为构造方法传入的可颁发许可证数量。

abstract static class Sync extends AbstractQueuedSynchronizer {
    Sync(int permits) {
        setState(permits);
    }
    //... 省略其它内容
}

到此处,可以知晓 Semaphore 内部实现是委托 AQS 同步器(Sync)实现,构造方法完成两件事

  • 确定同步器实现,公平锁(FairSync)与非公平锁(NonfairSync)
  • 初始化同步状态

接着查看颁发许可证的源码实现,acquire 方法其实提供了很多个,原理都是一样的,这里列举出几个。可以看出方法体都是一行代码,意思就是委托给 AQS 同步器。

// 方法1,阻塞式加锁,可响应中断
public void acquire() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}
// 方法2,阻塞式加锁,不可响应中断
public void acquireUninterruptibly() {
    sync.acquireShared(1);
}
// 方法3,快速失败式加锁
public boolean tryAcquire() {
    return sync.nonfairTryAcquireShared(1) >= 0;
}

上面的方法1、方法2调用的是 AQS 原始提供的加锁方法,方法3调用的是 AQS 实现类 Sync 自实现的方法。AQS 提供的加锁方法最终还是会调用 Sync 自实现的方法执行加锁,只是 AQS 还提供了一套完整的等待队列机制,就是说加锁不成功就会进入等待队列阻塞式等待,所以具体能否加锁成功还是得看 Sync 是怎么实现的。

非公平锁的加锁逻辑也比较简单,就是通过死循环不断地使用 cas 操作同步状态,操作成功则加锁成功,直到同步状态小于 0,加锁失败,此时方法3返回加锁失败,由线程自行决定如何操作,而执行方法1、2的线程则会自动进入 AQS 等待队列。

final int nonfairTryAcquireShared(int acquires) {
    for (;;) {
        int available = getState(); // 获取同步状态值
        int remaining = available - acquires; // 减掉 acquires
        if (remaining < 0 || // 减掉后必须大于等于 0
            compareAndSetState(available, remaining)) // cas 同步状态
            return remaining;
    }
}

最后查看归还许可证的源码实现,直接调用了 AQS 的 releaseShared 方法,这个方法会调用 tryReleaseShared 方法执行归还操作。

public void release() {
    sync.releaseShared(1);
}
// Sync 类的方法
protected final boolean tryReleaseShared(int releases) {
    for (;;) {
        int current = getState(); // 获取同步状态值
        int next = current + releases; // 加上 releases
        if (next < current) // 处理 int 越界异常
            throw new Error("Maximum permit count exceeded");
        if (compareAndSetState(current, next)) // cas 同步状态
            return true;
    }
}

AQS 本身提供了一整套的阻塞等待队列机制,加锁机制只需要通过控制同步状态即可,所以从代码实现来看是非常简单的。

前文中提到了公平锁与非公平锁的区别在于加锁时是否加入竞争,所以公平锁加锁时首先调用 hasQueuedPredecessors() 方法判断等待队列是否为空,如果不为空,则加锁失败,线程自动进入 AQS 等待队列。hasQueuedPredecessors() 方法也是 AQS 提供的方法,可以看出 AQS 已经把所有的事情都做完了。

static final class FairSync extends Sync {
    FairSync(int permits) {
        super(permits);
    }
    protected int tryAcquireShared(int acquires) {
        for (;;) {
            if (hasQueuedPredecessors()) // 首先判断等待队列是否为空,如果为空则加锁失败,线程进入等待队列。
                return -1;
            int available = getState();
            int remaining = available - acquires;
            if (remaining < 0 ||
                compareAndSetState(available, remaining))
                return remaining;
        }
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值