Semaphore

简单介绍

信号量,这个类的作用有点类似于“许可证”。有时,我们因为一些原因需要控制同时访问共享资源的最大线程数量,比如出于系统性能的考虑需要限流,或者共享资源是稀缺资源,我们需要有一种办法能够协调各个线程,以保证合理的使用公共资源。

Semaphore维护了一个许可集,其实就是一定数量的“许可证”。当有线程想要访问共享资源时,需要先获取(acquire)的许可;如果许可不够了,线程需要一直等待,直到许可可用。当线程使用完共享资源后,可以归还(release)许可,以供其它需要的线程使用。

另外,Semaphore支持公平/非公平策略,这和ReentrantLock类似,后面讲Semaphore原理时会看到,它们的实现本身就是类似的。

简单使用

package com.liaoxiang.multithreading3.middle.aqs_lock;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

/**
 * 信号量-Semaphore
 * 主要方法:
 *      semaphore.acquire()
 *      semaphore.release()
 */
public class SemaphoreTest {
    public static void main(String[] args) {
        // thread_pool
        ExecutorService exec = Executors.newCachedThreadPool();
        // 只能5个线程同时访问
        final Semaphore semaphore = new Semaphore(5);
        // 模拟20个客户端访问
        for (int index = 1; index <= 10; index++) {
            final int NO = index;
            exec.execute(() -> {
                try {
                    // 获取许可
                    semaphore.acquire();
                    System.out.println("业务执行中: " + NO + "...");
                    //模拟实际业务逻辑
                    Thread.sleep((long) (Math.random() * 5000));
                    // 释放许可
                    semaphore.release();
                    System.out.println("执行完成了: " + NO);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        try {
			Thread.sleep(10);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
        // 退出线程池
        exec.shutdown();
    }
}

在这里插入图片描述
执行效果:首先有五个线程同时执行任务,如果有一个线程执行完成,立马加入一个新的线程继续执行,直到所有线程执行完成

原理分析

熟悉的味道。。。Semaphore果然是通过内部类实现了AQS框架提供的接口,而且基本结构几乎和ReentrantLock完全一样,通过两个内部类分别实现了公平/非公平策略。
在这里插入图片描述

构造器

创建一个给定许可数量的信号量对象,默认使用非公平锁,通过第二个参数可设置为公平锁

    /**
     * Creates a {@code Semaphore} with the given number of
     * permits and nonfair fairness setting.
     *
     * @param permits the initial number of permits available.
     *        This value may be negative, in which case releases
     *        must occur before any acquires will be granted.
     */
    public Semaphore(int permits) {
        sync = new NonfairSync(permits);
    }

    /**
     * Creates a {@code Semaphore} with the given number of
     * permits and the given fairness setting.
     *
     * @param permits the initial number of permits available.
     *        This value may be negative, in which case releases
     *        must occur before any acquires will be granted.
     * @param fair {@code true} if this semaphore will guarantee
     *        first-in first-out granting of permits under contention,
     *        else {@code false}
     */
    public Semaphore(int permits, boolean fair) {
        sync = fair ? new FairSync(permits) : new NonfairSync(permits);
    }

主要方法

  • 1、Semaphore#acquire()
public void acquire() throws InterruptedException {
    sync.acquireSharedInterruptibly(1); // 调用父类AQS中的获取共享锁资源的方法
}
  • 2、AbstractQueuedSynchronizer#acquireSharedInterruptibly()
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
	// 调用之前先检测该线程中断标志位,检测该线程在之前是否被中断过
    if (Thread.interrupted()) 
     	// 若被中断过的话,则抛出中断异常
        throw new InterruptedException();
    // 尝试获取共享资源锁,小于0则获取失败,此方法由AQS的具体子类实现
    if (tryAcquireShared(arg) < 0) 
    	// 将尝试获取锁资源的线程进行入队操作
        doAcquireSharedInterruptibly(arg); 
}
  • 3、Semaphore.FairSync#tryAcquireShared()——公平
protected int tryAcquireShared(int acquires) {
    for (;;) { // 自旋的死循环操作方式
        if (hasQueuedPredecessors()) // 检查线程是否有阻塞队列
            return -1; // 如果有阻塞队列,说明共享资源的许可数量已经用完,返回-1乖乖进行入队操作
        int available = getState(); // 获取staste
        int remaining = available - acquires; // 计算得到剩下的许可数量
        if (remaining < 0 || // 若剩下的许可数量小于0,说明已经共享资源了,返回负数然后乖乖进入入队操作
            compareAndSetState(available, remaining)) // 若共享资源大于或等于0,CAS操作占据最后许可证
            //不管得到remaining后进入了何种逻辑,操作了之后再将remaining返回
            //2、方法会根据remaining的值进行判断是否需要入队操作
            return remaining;
    }
}
public final boolean hasQueuedPredecessors() {
    // The correctness of this depends on head being initialized
    // before tail and on head.next being accurate if the current
    // thread is first in queue.
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}
  • 4、Semaphore.NonfairSync#tryAcquireShared()——非公平
protected int tryAcquireShared(int acquires) {
    return nonfairTryAcquireShared(acquires);
}
  • 5、Semaphore.Sync#nonfairTryAcquireShared()
final int nonfairTryAcquireShared(int acquires) {
    for (;;) { 
        int available = getState();
        int remaining = available - acquires; 
        if (remaining < 0 || 
            compareAndSetState(available, remaining))
            return remaining;
    }
}  

与公平区别就是:非公平不会管队列中是否有在排队的线程,直接符合条件直接CAS

回到第2步,如果获取共享资源锁失败,则执行doAcquireSharedInterruptibly(arg);方法

public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
	// 调用之前先检测该线程中断标志位,检测该线程在之前是否被中断过
    if (Thread.interrupted()) 
     	// 若被中断过的话,则抛出中断异常
        throw new InterruptedException();
    // 尝试获取共享资源锁,小于0则获取失败,此方法由AQS的具体子类实现
    if (tryAcquireShared(arg) < 0) 
    	// 将尝试获取锁资源的线程进行入队操作
        doAcquireSharedInterruptibly(arg); 
}
  • 6、AbstractQueuedSynchronizer#doAcquireSharedInterruptibly()
/**
 * Acquires in shared interruptible mode.
 * @param arg the acquire argument
 */
private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {
    // 按照给定的mode模式创建新的结点,模式有两种:Node.EXCLUSIVE独占模式、Node.SHARED共享模式;
    final Node node = addWaiter(Node.SHARED); // 6.1 加入队列,返回值是以当前线程创建的Node节点
    boolean failed = true;
    try {
        for (;;) { // 自旋
            final Node p = node.predecessor(); // 获取当前结点的前驱结点
            if (p == head) { // 若前驱结点为head的话,则当前节点时队列中的第一个节点,最先尝试获取锁
                int r = tryAcquireShared(arg); 
                if (r >= 0) { // 若r >= 0,说明已经成功的获取到了共享锁资源
                    setHeadAndPropagate(node, r); // 把当前node结点设置为头结点,并且调用doReleaseShared释放一下无用的结点
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) && // 是否需要挂起
                parkAndCheckInterrupt()) // 阻塞操作,正常情况下,获取不到共享锁,代码就在该方法停止了,直到被唤醒
                // 被唤醒后,发现parkAndCheckInterrupt()里面检测了被中断了的话,则补上中断异常,因此抛了个异常
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

AbstractQueuedSynchronizer#addWaiter()方法

private Node addWaiter(Node mode) {
	//创建当前线程节点
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    //创建pred节点指向尾节点
    Node pred = tail;
    //尾节点不为null时
    if (pred != null) {
    	//让当前线程节点的前驱节点指向pred节点(尾节点)
        node.prev = pred;
        //CAS交换pred和node节点
        if (compareAndSetTail(pred, node)) {
        	//pred的后继节点指向node
            pred.next = node;
            //返回当前线程节点
            return node;
        }
    }
    //注意!到达这里有两种情况,一是尾节点为null(队列为空)
    //二是CAS替换尾节点失败(因为这个方法是多个线程一起执行的,CAS可能成功,可能失败)
    enq(node);
    //返回当前线程节点
    return node;
}

AbstractQueuedSynchronizer#enq()方法

private Node enq(final Node node) {
    for (;;) {
    	//尾节点赋值给t
        Node t = tail;
        //如果尾节点为null,则为空队列
        //从这里可以看出, 队列不是在构造的时候初始化的, 而是延迟到需要用的时候再初始化
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
            	// 这里仅仅是将尾节点指向dummy节点(head),并没有返回
                tail = head;
        } else {
        	// 到这里说明队列已经不是空的了, 这个时候再继续尝试将节点加到队尾
            node.prev = t;
            //交换尾节点和当前节点
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

上面两个方法在ReentrantLock中已给出分析

  • 7、Semaphore#release()
public void release() {
    sync.releaseShared(1); // 释放一个许可资源 
}
  • 8、AbstractQueuedSynchronizer#releaseShared()
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) { // 尝试释放共享锁资源,此方法由AQS的具体子类实现
        doReleaseShared(); // 自旋操作,唤醒后继结点
        return true;
    }
    return false;
}
  • 9、Semaphore.Sync#tryReleaseShared
protected final boolean tryReleaseShared(int releases) {
    for (;;) { // 自旋
        int current = getState(); // 获取最新的共享锁资源值
        int next = current + releases; // 对许可数量进行加法操作
        // int类型值小于0,是因为该int类型的state状态值溢出了,溢出了的话那得说明这个锁有多难释放啊,可能出问题了
        if (next < current) // overflow
            throw new Error("Maximum permit count exceeded");
        if (compareAndSetState(current, next)) // 
            return true; // 返回成功标志,告诉上层该线程已经释放了共享锁资源
    }
}
  • 10、AbstractQueuedSynchronizer#doReleaseShared()
private void doReleaseShared() {
    for (;;) { // 自旋
        Node h = head; // 每次都是取出队列的头结点
        if (h != null && h != tail) { // 若头结点不为空且也不是队尾结点
            int ws = h.waitStatus; // 那么则获取头结点的waitStatus状态值
            if (ws == Node.SIGNAL) { // 若头结点是SIGNAL状态则意味着头结点的后继结点需要被唤醒了
                // 通过CAS尝试设置头结点的状态为空状态,失败的话,则继续循环,因为并发有可能其它地方也在进行释放操作
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)){
                	continue;   // loop to recheck cases
                }
                unparkSuccessor(h); // 唤醒头结点的后继结点
            }
            // 如头结点为空状态,则把其改为PROPAGATE状态,失败的则可能是因为并发而被改动过,则再次循环处理
            else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        // 若头结点没有发生什么变化,则说明上述设置已经完成
        // 若发生了变化,可能是操作过程中头结点有了新增或者啥的,那么则必须进行重试,以保证唤醒动作可以延续传递
        if (h == head)                   // loop if head changed
            break;
    }
}

总结

Semaphore其实就是实现了AQS共享功能的同步器,对于Semaphore来说,资源就是许可证的数量:

  • 剩余许可证数(State值) - 尝试获取的许可数(acquire方法入参) ≥ 0:资源可用
  • 剩余许可证数(State值) - 尝试获取的许可数(acquire方法入参) < 0:资源不可用

这里共享的含义是多个线程可以同时获取资源,当计算出的剩余资源不足时,线程就会阻塞。
注意:Semaphore不是锁,只能限制同时访问资源的线程数,至于对数据一致性的控制,Semaphore是不关心的。当前,如果是只有一个许可的Semaphore,可以当作锁使用
参考:J.U.C之synchronizer框架:Semaphore

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值