java.util.concurrent.Semaphore(信号量)原理解析

一、引言

Semaphore又称信号量,用来控制同时访问访问某个特定资源的操作数量,可以理解为是用于控制线程访问数量的工具类。
Semaphore管理着一组许可(permit),许可的数量可通过构造函数来指定。在执行某个操作时必须先获得许可,并在使用完毕后释放(release)许可。如果没有许可,那么尝试获取许可的线程会进入阻塞状态直到有许可可以使用。

可以想象成一个房间,房间里只能容纳3个人(每个人相当于一个线程)。现在外面的人想要进来(尝试获取许可),如果房间没有满,则可以直接进入(获取许可成功,线程继续执行),如果房间已经满了,则必须在外面等待直到有人出来(线程阻塞,当有其它获取许可的线程执行完毕释放许可后,当前线程才可继续执行)。

Semaphore构造方法如下:

方法名解释
public Semaphore(int permits)permits为许可数量(相当于房间中能容纳的人)
public Semaphore(int permits, boolean fair)与上述方法类似,但这里多了一个参数:是否公平,即等待时间最长的线程优先获得许可

获取许可和释放许可的方法:

方法名作用
void acquire() throws InterruptedException获取单个许可,若没有获取到则当前线程阻塞
void acquire(int permits) throws InterruptedException获取permits个许可,若没有获取到则当前线程阻塞
void release()释放一个许可
void release(int permits)释放permits个许可
boolean tryAcquire()尝试获取许可,若成功则立即返回true,否则返回false
boolean tryAcquire(long timeout, TimeUnit unit)
throws InterruptedException
尝试获取许可,若在指定时间内成功获得,则返回true,否则返回false
boolean tryAcquire(int permits)尝试获取permits个许可,若成功则返回true,否则返回false
boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException尝试获取permits个许可,若在指定时间内成功获得,则返回true,否则返回false
int availablePermits()获取当前许可数目

下面为Semaphore简单使用示范

public class TestTask{
    private final ExecutorService exec;
    private final Semaphore semaphore;

    public TestTask(int nThread){
        exec = Executors.newFixedThreadPool(nThread);
        semaphore = new Semaphore(nThread);
    }

    private final class Man implements Runnable{
        private int id;
        public Man(int id) {this.id = id;}

        @Override
        public void run() {
            try{
                semaphore.acquire(); //获取许可
                System.out.println("ID:" + id + "在此房间,剩余许可:" + semaphore.availablePermits());
            }
            catch (InterruptedException e){
            }finally{
                semaphore.release(); //释放许可
                System.out.println("ID:" + id + "走出房间,剩余许可:" + semaphore.availablePermits());
            }
        }
    }

    public void init() throws InterruptedException, ExecutionException{
        for(int i = 1; i <= 8; i++) //一共8个人想进入房间
            exec.submit(new Man(i), true);
        exec.shutdown();
    }

    public static void main(String[] args) throws InterruptedException, ExecutionException{
        TestTask task = new TestTask(4); //设置线程数,相当于房间最大能容纳4个人
        task.init();
    }
}

输出:

ID:3在此房间,剩余许可:1
ID:2在此房间,剩余许可:1
ID:1在此房间,剩余许可:1
ID:4在此房间,剩余许可:0
ID:1走出房间,剩余许可:3
ID:2走出房间,剩余许可:2
ID:3走出房间,剩余许可:1
ID:5在此房间,剩余许可:3
ID:7在此房间,剩余许可:1
ID:4走出房间,剩余许可:4
ID:7走出房间,剩余许可:3
ID:5走出房间,剩余许可:2
ID:6在此房间,剩余许可:2
ID:8在此房间,剩余许可:2
ID:6走出房间,剩余许可:3
ID:8走出房间,剩余许可:4

了解完基本使用方法后,如果你不了解Java并发框架AbstractQueuedSynchronizer(AQS)的基本原理,可以参考我的博客来熟悉AQS:
https://blog.csdn.net/abc123lzf/article/details/82532036

如果你彻底理解了AQS,那么Semaphore对于你来说肯定是很简单的。

二、原理分析

Semaphore基本结构和ReentrantLock非常类似,都有一个内部类Sync继承了AQS,其子类NonfairSync和FairSync分别实现非公平竞争模式和公平竞争模式。

public class Semaphore implements java.io.Serializable {
    private final Sync sync;

    abstract static class Sync extends AbstractQueuedSynchronizer {
        //...
    }

    static final class NonfairSync extends Sync {
        //...
    }

    static final class FairSync extends Sync {
        //...
    }
}

Semaphore类只有一个实例变量sync,这个类的所有方法都是基于这个Sync的同步器实现的。

对于Semaphore的内部类Sync来说,AQS的state变量代表剩余的许可数量

1、acquire方法

acquire方法有两个重载的方法:acquire()和acquire(int),前者会尝试获取1个许可(资源),后者会尝试获取指定的许可(资源)数量(但不可小于0)
我们只分析acquire():

public void acquire() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

acquire方法它实际上调用了Sync类的acquireSharedInterruptibly方法,acquireSharedInterruptibly方法的实现在AQS中。

public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
    //如果当前线程被打断则抛出异常
    if (Thread.interrupted())
        throw new InterruptedException();
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}

该方法会尝试调用tryAcquireShared方法获取资源,若没有获取到(返回值小于0),则加入到AQS的线程等待队列中等待。
tryAcquireShared方法并没有在Sync类中重写,而是实现在子类NonfairSync和FairSync中。

NonfairSync的实现:

//acquire参数为申请的许可数量
protected int tryAcquireShared(int acquires) {
    return nonfairTryAcquireShared(acquires);
}

//该方法实现在Sync类中
final int nonfairTryAcquireShared(int acquires) {
    //通过循环反复尝试(自旋)
    for (;;) {
        //获取AQS的state变量
        int available = getState();
        int remaining = available - acquires;
        //如果remaining小于0(获取资源失败)
        //或成功通过CAS将其由旧的state改为remaining(获取资源成功,返回值大于0)
        if (remaining < 0 || compareAndSetState(available, remaining))
            return remaining;
    }
}

Sync重写了AQS的tryReleaseShared方法,即释放资源的方法,获取资源的方法tryAcquireShared由子类实现。
nonfairTryAcquireShared非公平实现方式和ReentrantLock类似,通过CAS操作抢占式将available设为remaining,而不管AQS的等待队列中是否有其它线程正在等待资源释放。

FairSync的实现:

protected int tryAcquireShared(int acquires) {
    for (;;) {
        //如果AQS的等待队列中有线程正在等待,则返回-1,资源(许可)获取失败
        if (hasQueuedPredecessors())
            return -1;
        //下面和NonfairSync的实现类似
        int available = getState();
        int remaining = available - acquires;
        if (remaining < 0 || compareAndSetState(available, remaining))
            return remaining;
    }
}

FairSync之所以能实现公平,是因为该方法会首先判断AQS的等待队列中是否有线程正在等待,如果没有才会尝试去自己获得资源,若有线程等待,则会将当前线程入队等待。

2、release方法

release同样也有2个重载的方法:release()和release(int),前者会尝试释放1个许可(资源),后者会尝试释放指定的许可(资源)数量(但不可小于0)

public void release() {
    sync.releaseShared(1);
}

releaseShared实现在AQS中:

public final boolean releaseShared(int arg) {
    //调用子类的tryReleaseShared方法尝试获取许可,大于0代表成功
    if (tryReleaseShared(arg)) {
        //通知等待队列的线程
        doReleaseShared();
        return true;
    }
    return false;
}

tryReleaseShared方法实现在Sync类中,Nonfair和FairSync都是调用该方法来释放资源:

protected final boolean tryReleaseShared(int releases) {
    for (;;) {
        //获取AQS的state
        int current = getState();
        int next = current + releases;
        //如果数溢出则抛出错误
        if (next < current)
            throw new Error("Maximum permit count exceeded");
        //通过CAS更新AQS的state变量,更新失败则重新尝试
        if (compareAndSetState(current, next))
            return true;
    }
}

tryReleaseShared方法的主要任务就是将AQS原先的state变量加上releases,表示许可(资源)的释放。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个简单的Java信号使用示例程序: ```java import java.util.concurrent.Semaphore; public class SemaphoreExample { public static void main(String[] args) { Semaphore semaphore = new Semaphore(3); // 创建一个信号,初始值为3 // 创建5个线程 for (int i = 1; i <= 5; i++) { Thread thread = new Thread(new Worker(semaphore, i)); thread.start(); } } static class Worker implements Runnable { private final Semaphore semaphore; private final int id; public Worker(Semaphore semaphore, int id) { this.semaphore = semaphore; this.id = id; } @Override public void run() { try { semaphore.acquire(); // 等待信号,如果信号为0则阻塞 System.out.println("Worker " + id + " acquired semaphore"); Thread.sleep(1000); // 模拟工作 } catch (InterruptedException e) { e.printStackTrace(); } finally { semaphore.release(); // 释放信号 System.out.println("Worker " + id + " released semaphore"); } } } } ``` 在这个示例程序中,创建了一个初始值为3的信号。然后创建了5个线程,每个线程都是一个Worker实例,Worker实例的构造函数中传入了信号和线程ID。每个线程运行时,首先尝试获取信号,如果信号为0,则阻塞等待。如果成功获取信号,就输出一个日志,然后模拟一秒钟的工作时间。最后释放信号并输出一个日志。 由于信号的初始值为3,所以前3个线程可以同时获取信号并运行,后面的两个线程需要等待前面的线程释放信号后才能获取。输出结果如下: ``` Worker 1 acquired semaphore Worker 2 acquired semaphore Worker 3 acquired semaphore Worker 1 released semaphore Worker 4 acquired semaphore Worker 2 released semaphore Worker 5 acquired semaphore Worker 3 released semaphore Worker 4 released semaphore Worker 5 released semaphore ``` 可以看到,前三个线程可以同时获取信号,后面的两个线程需要等待前面的线程释放信号后才能获取。这就是信号的作用。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值