Semaphore 相关整理

1. 简介

  • Semaphore 类是一个计数信号量,必须由获取它的线程释放, 通常用于限制可以访问某些资源(物理或逻辑的)线程数目。
  • 一个信号量有且仅有 3 种操作,且它们全部是原子的。
    • 初始化、增加和减少。
    • 增加可以为一个进程解除阻塞。
    • 减少可以让一个进程进入阻塞。
  • Semaphore 管理一系列许可证。
    • 每个 acquire() 方法阻塞,直到有一个许可证可以获得然后拿走一个许可证。
    • 每个 release() 方法增加一个许可证,这可能会释放一个阻塞的 acquire() 方法。
    • 不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动。
  • Semaphore 在计数器不为 0 的时候对线程就放行,一旦达到 0,那么所有请求资源的新线程都会被阻塞,包括增加请求到许可的线程,Semaphore 是不可重入的。
    • 每一次请求一个许可都会导致计数器减少 1,同样每次释放一个许可都会导致计数器增加 1,一旦达到 0,新的许可请求线程将被挂起。
  • Semaphore 有两种模式,公平模式非公平模式
    • 公平模式就是调用 acquire 的顺序就是获取许可证的顺序,遵循 FIFO。
    • 非公平模式是抢占式的,也就是有可能一个新的获取线程恰好在一个许可证释放时得到了这个许可证,而前面还有等待的线程。

1.1 Semaphore 的应用场景

  • 比如模拟一个停车场停车信号,假设停车场只有两个车位,一开始两个车位都是空的。这时同时来了两辆车,看门人允许它们进入停车场,然后放下车拦。以后来的车必须在入口等待,直到停车场中有车辆离开。这时,如果有一辆车离开停车场,看门人得知后,打开车拦,放入一辆,如果又离开一辆,则又可以放入一辆,如此往复。
public class SemaphoreDemo {

    private static Semaphore s = new Semaphore(2);

    static class ParkTask implements Runnable {
        private String name;

        public ParkTask(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            try {
                s.acquire();
                System.out.println("Thread " + this.name + " start...");
                TimeUnit.SECONDS.sleep(new Random().nextInt(10));
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                s.release();
            }
        }

        public static void main(String[] args) {
            ExecutorService pool = Executors.newCachedThreadPool();
            pool.submit(new ParkTask("1"));
            pool.submit(new ParkTask("2"));
            pool.submit(new ParkTask("3"));
            pool.submit(new ParkTask("4"));
            pool.submit(new ParkTask("5"));
            pool.submit(new ParkTask("6"));
            pool.shutdown();
        }
}

/**
--- print ---
Thread 2 start...
Thread 6 start...
Thread 3 start...
Thread 1 start...
Thread 4 start...
Thread 5 start...
*/

2. Semaphore 原理

 
Semaphore 类图
  • Semaphore 通过使用内部类 Syn 继承 AQS 实现。
  • 包含的方法函数。
// 创建具有给定的许可数和非公平的公平设置的 Semaphore。
Semaphore(int permits)
// 创建具有给定的许可数和给定的公平设置的 Semaphore。
Semaphore(int permits, boolean fair)

// 从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断。
void acquire()
// 从此信号量获取给定数目的许可,在提供这些许可前一直将线程阻塞,或者线程已被中断。
void acquire(int permits)
// 从此信号量中获取许可,在有可用的许可前将其阻塞。
void acquireUninterruptibly()
// 从此信号量获取给定数目的许可,在提供这些许可前一直将线程阻塞。
void acquireUninterruptibly(int permits)
// 返回此信号量中当前可用的许可数。
int availablePermits()
// 获取并返回立即可用的所有许可。
int drainPermits()
// 返回一个 collection,包含可能等待获取的线程。
protected Collection<Thread> getQueuedThreads()
// 返回正在等待获取的线程的估计数目。
int getQueueLength()
// 查询是否有线程正在等待获取。
boolean hasQueuedThreads()
// 如果此信号量的公平设置为 true,则返回 true。
boolean isFair()
// 根据指定的缩减量减小可用许可的数目。
protected void reducePermits(int reduction)
// 释放一个许可,将其返回给信号量。
void release()
// 释放给定数目的许可,将其返回到信号量。
void release(int permits)
// 返回标识此信号量的字符串,以及信号量的状态。
String toString()
// 仅在调用时此信号量存在一个可用许可,才从信号量获取许可。
boolean tryAcquire()
// 仅在调用时此信号量中有给定数目的许可时,才从此信号量中获取这些许可。
boolean tryAcquire(int permits)
// 如果在给定的等待时间内此信号量有可用的所有许可,并且当前线程未被中断,则从此信号量获取给定数目的许可。
boolean tryAcquire(int permits, long timeout, TimeUnit unit)
// 如果在给定的等待时间内,此信号量有可用的许可并且当前线程未被中断,则从此信号量获取一个许可。
boolean tryAcquire(long timeout, TimeUnit unit)

构造函数

  • 两个构造方法,都必须提供许可的数量,第二个构造方法可以指定是公平模式还是非公平模式,默认非公平模式。
//permits是允许同时运行的线程数目
public Semaphore(int permits) {
        sync = new NonfairSync(permits);
}

public Semaphore(int permits, boolean fair) {
        sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
  • Semaphore 内部基于 AQS 的共享模式,所有实现委托给 Sync 类完成。
NonfairSync(int permits) {
      super(permits);
}
......
Sync(int permits) {
      setState(permits);
}
  • Semaphore 提供了两种获取资源的方式。
    • 响应中断不响应中断

响应中断获取资源

  • 两个方法支持 Interrupt 中断机制,可使用 acquire() 方法每次获取一个信号量,也可以使用 acquire(int permits) 方法获取指定数量的信号量 。
//从semaphore中获取一个许可,线程会一直被阻塞直到获取一个许可或是被中断,获取一个许可后立即返回,并把许可数减1,如果没有可用的许可,当前线程会处于休眠状态直到
//1.某些其他线程调用release方法,并且当前线程是下一个要被分配许可的线程
//2.某些其他线程中断当前线程
//如果当前线程被acquire方法使得中断状态设置为on或者在等待许可时被中断则抛出InterruptedException,并且清除当前线程的已中断状态
public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
}
public void acquire(int permits) throws InterruptedException {
    if (permits < 0) throw new IllegalArgumentException();
    sync.acquireSharedInterruptibly(permits);
}
  • AQS 子类使用共享模式,需要实现 tryAcquireShared() 方法。
    • 在公平锁中还是与 ReentrantLock 中的操作一样,先判断同步队列中是不是还有其他的等待线程,有则直接返回失败。
      • 否则对 state 值进行减操作并返回剩下的信号量。
    • 非公平锁直接调用了父类中的 nonfairTryAcquireShared 和 ReentrantLock 一样。
// 非公平锁的获取方式
protected int tryAcquireShared(int acquires) {
    return nonfairTryAcquireShared(acquires);
}


final int nonfairTryAcquireShared(int acquires) {
    for (;;) {
        int available = getState();//获取去中的信号量数
        int remaining = available - acquires;//剩余信号量数
        //1.信号量数大于0,获取共享锁,并设置执行compareAndSetState(available, remaining),返回剩余信号量数
        //2.信号量数小于等于0,直接返回负数
        if (remaining < 0 || compareAndSetState(available, remaining))
            return remaining;
    }
}

// 公平锁获取
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;
    }
}
  • 变量 state 采用 volatile 可见修饰。
/**
  * The synchronization state.
*/
private volatile int state;
 
/**
 * Returns the current value of synchronization state.
 * This operation has memory semantics of a <tt>volatile</tt> read.
 * @return current state value
*/
protected final int getState() {
    return state;
}

不响应中断获取资源

  • 两个方法不响应 Interrupt 中断机制,其它功能与 acquire() 方法一致。
//从semaphore中获取一个许可,线程会一直被阻塞直到获取一个许可或是被中断,获取一个许可后立即返回,并把许可数减1,如果没有可用的许可,当前线程会处于休眠状态直到
//1.某些其他线程调用release方法,并且当前线程是下一个要被分配许可的线程
//2.如果当前线程在等待许可时被中断,那么它会接着等待,但是与没有发生中断相比,为线程分配许可的时间可能改变 
ublic void acquireUninterruptibly() {
    sync.acquireShared(1);
}

public void acquireUninterruptibly(int permits) {
    if (permits < 0) throw new IllegalArgumentException();
    sync.acquireShared(permits);
}!

尝试获得信号量

  • 尝试获得信号量有三个方法。
    • 尝试获取信号量,如果获取成功则返回 true,否则马上返回 false,不会阻塞当前线程。
    • 尝试获取信号量,如果在指定的时间内获得信号量,则返回 true,否则返回 false。
    • 尝试获取指定数量的信号量,如果在指定的时间内获得信号量,则返回 true,否则返回 false。
public boolean tryAcquire() {
    return sync.nonfairTryAcquireShared(1) >= 0;
}

public boolean tryAcquire(long timeout, TimeUnit unit)
        throws InterruptedException {
    return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

public boolean tryAcquire(int permits, long timeout, TimeUnit unit)
        throws InterruptedException {
    if (permits < 0) throw new IllegalArgumentException();
    return sync.tryAcquireSharedNanos(permits, unit.toNanos(timeout));
}

释放资源

  • release 方法,主要作用是释放资源,需要保证 release 的执行,否则线程退出但是资源没有释放。
    • 一般代码写在 finally 中是最好的。
    • 并且获取多少资源就要释放多少资源,否则还是资源没被正确释放,如果一开始执行了 acquire(10) 最后释放的时候不能只写一个 release() 而是 release(10) 才对。
//  尝试释放锁
public final boolean release(int arg) {
    // 如果释放锁成功 唤醒同步队列中的后继节点
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
// 为了方便对比把两个代码放在一块 可以看到 release 中的结构完全一样
// 区别就在于 doReleaseShared 中有更多的判断操作
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();  //在里面执行的 unparkSuccessor(h)
        return true;
    }
    return false;
}
  • 子类实现共享模式的类需要实现 tryReleaseShared() 方法判断是否释放成功。
    • 这个方法是一个 CAS 自旋,原因是应为 Semaphore 是一个共享锁,可能有多个线程同时释放资源,因此 CAS 操作可能失败。
protected final boolean tryReleaseShared(int releases) {
    for (;;) {
        //获取当前许可数量
        int current = getState();
        //计算回收后的数量
        int next = current + releases;
        if (next < current) // overflow
            throw new Error("Maximum permit count exceeded");
        //CAS改变许可数量成功,返回true
        if (compareAndSetState(current, next))
            return true;
    }
}
  • 一旦 CAS 改变许可数量成功,就调用 doReleaseShared() 方法释放阻塞的线程。

其他方法

获取当前剩余的信号量数量

  • 该方法返回 AQS 中 state 变量的值,当前剩余的信号量个数。
public int availablePermits() {
    return sync.getPermits();
}

// Sync
final int getPermits() {
    return getState();
}

耗尽许可数量

  • 获取并返回立即可用的所有许可。
  • Sync 类的 drainPermits() 方法,获取 1 个信号量后将可用的信号量个数置为 0。
    • 例如总共有 10 个信号量,已经使用了 5 个,再调用 drainPermits() 方法后,可以获得一个信号量,剩余 4 个信号量就消失了,总共可用的信号量就变成 6 个了。
    • 用 CAS 自旋将剩余资源清空。
public int drainPermits() {
    return sync.drainPermits();
}

// Sync
final int drainPermits() {
    for (;;) {
        int current = getState();
        if (current == 0 || compareAndSetState(current, 0))
            return current;
    }
}

缩减许可数量

  • 缩减必须是单向的,即只能减少不能增加。
    • 用 CAS 自旋在剩余共享资源上做缩减。
protected void reducePermits(int reduction) {
    if (reduction < 0) throw new IllegalArgumentException();
    sync.reducePermits(reduction);
}

// Sync
final void reducePermits(int reductions) {
    for (;;) {
        int current = getState();
        int next = current - reductions;
        if (next > current) // underflow
            throw new Error("Permit count underflow");
        if (compareAndSetState(current, next))
            return;
    }
}
  • 上述两个方法对共享资源数量的修改操作有两点需要注意
    • 是不可逆的
    • 是对剩余资源的操作而不是全部资源,当剩余资源数目不足或已经为 0 时,方法就返回。
    • 正在被占用的资源不参与。

判断 AQS 同步队列中是否还有 Node

public final boolean hasQueuedThreads() {
    return sync.hasQueuedThreads();
}

// AbstractQueuedSynchronizer
public final boolean hasQueuedThreads() {
   //头结点不等于尾节点就说明链表中还有元素
   return head != tail;
}

3. 总结

  • Semaphore 的内部工作流程也是基于 AQS,不同于 CyclicBarrier 和 ReentrantLock,不会使用到 AQS 的条件队列,都是在同步队列中操作,只是当前线程会被 park。
  • Semaphore 是 JUC 包提供的一个典型的共享锁,它通过自定义两种不同的同步器(FairSync 和 NonfairSync)提供了公平和非公平两种工作模式,两种模式下分别提供了限时/不限时、响应中断/不响应中断的获取资源的方法(限时获取总是及时响应中断的),而所有的释放资源的 release() 操作是统一的。

参考资料

https://www.meiwen.com.cn/subject/expkcxtx.html
https://blog.csdn.net/u014634338/article/details/78701445



作者:羽杰
链接:https://www.jianshu.com/p/0572483ecec7
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
posted @ 2020-03-26 13:38  windy杨树  阅读(487)  评论(0编辑  收藏  举报