深入理解AQS之Semaphorer


并发编程系列


Semaphore介绍

Semaphore,俗称信号量,它是操作系统中PV操作的原语在java的实现,它也是基于AbstractQueuedSynchronizer实现的。
Semaphore的功能非常强大,大小为1的信号量就类似于互斥锁,通过同时只能有一个线程获取信号量实现。大小为n(n>0)的信号量可以实现限流的功能,它可以实现只能有n个线程同时获取信号量。
在这里插入图片描述

PV操作是操作系统一种实现进程互斥与同步的有效方法。PV操作与信号量(S)的处理相关,P表示通过的意思,V表示释放的意思。用PV操作来管理共享资源时,首先要确保PV操作自身执行的正确性。
P操作的主要动作是:
①S减1;
②若S减1后仍大于或等于0,则进程继续执行;
③若S减1后小于0,则该进程被阻塞后放入等待该信号量的等待队列中,然后转进程调度。
V操作的主要动作是:
①S加1;
②若相加后结果大于0,则进程继续执行;
③若相加后结果小于或等于0,则从该信号的等待队列中释放一个等待进程,然后再返回原进程继续执行或转进程调度。

Semaphore 常用方法

构造器
在这里插入图片描述

  • permits 表示许可证的数量(资源数)
  • fair 表示公平性,如果这个设为 true 的话,下次执行的线程会是等待最久的线程

常用方法

public void acquire() throws InterruptedException
public boolean tryAcquire()
public void release()
public int availablePermits()
public final int getQueueLength() 
public final boolean hasQueuedThreads()
protected void reducePermits(int reduction)
  • acquire() 表示阻塞并获取许可
  • tryAcquire() 方法在没有许可的情况下会立即返回 false,要获取许可的线程不会阻塞
  • release() 表示释放许可
  • int availablePermits():返回此信号量中当前可用的许可证数。
  • int getQueueLength():返回正在等待获取许可证的线程数。
  • boolean hasQueuedThreads():是否有线程正在等待获取许可证。
  • void reducePermit(int reduction):减少 reduction 个许可证
  • Collection getQueuedThreads():返回所有等待获取许可证的线程集合

应用场景

可以用于做流量控制,特别是公用资源有限的应用场景

限流
package com.syn;

import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.Semaphore;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class SemaphoneTest2 {

    /**
     * 实现一个同时只能处理5个请求的限流器
     */
    private static Semaphore semaphore = new Semaphore(5);

    /**
     * 定义一个线程池
     */
    private static ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 50, 60, TimeUnit.SECONDS, new LinkedBlockingDeque<>(200));

    /**
     * 模拟执行方法
     */
    public static void exec() {
        try {
            semaphore.acquire(1);
            // 模拟真实方法执行
            System.out.println("执行exec方法");
            Thread.sleep(2000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            semaphore.release(1);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        {
            for (; ; ) {
                Thread.sleep(100);
                // 模拟请求以10个/s的速度
                executor.execute(() -> exec());
            }
        }
    }
}

Semaphore源码分析

关注点:

  1. Semaphore的加锁解锁(共享锁)逻辑实现
  2. 线程竞争锁失败入队阻塞逻辑和获取锁的线程释放锁唤醒阻塞线程竞争锁的逻辑实现

在这里插入图片描述
在这里插入图片描述
从以上两张图不难看出,Semaphore的功能主要是靠sync属性来实现的,而sync无论是公平实现还是非公平实现,最终又都是通过继承AQS来实现的。

调用acquire方式其实调用的是sync的acquireSharedInterruptibly方法,这里是共享锁实现。

public void acquire(int permits) throws InterruptedException {
    if (permits < 0) throw new IllegalArgumentException();
    sync.acquireSharedInterruptibly(permits);
}

acquireSharedInterruptibly方法在AQS中实现,子类不需要实现。

public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}

这个方法的逻辑就是首先通过tryAcquireShared尝试获取锁,如果这个方法返回值大于等于0,就是获取锁成功,继续接下来的业务逻辑,而如果这个方法返回小于0,就会考虑进入等待队列进行等待的逻辑了,这些逻辑都是公共的,doAcquireSharedInterruptibly也是AQS的公共实现。

private void doAcquireSharedInterruptibly(int arg)
     throws InterruptedException {
     1. 添加一个节点到等待队列
     final Node node = addWaiter(Node.SHARED);
     boolean failed = true;
     try {
         for (;;) {
             final Node p = node.predecessor();
             if (p == head) {
                 2. 如果当前节点的前一个节点是头节点 再次尝试获取锁 获取成功需要返回数字大于0 则返回
                 int r = tryAcquireShared(arg);
                 if (r >= 0) {
                     setHeadAndPropagate(node, r);
                     p.next = null; // help GC
                     failed = false;
                     return;
                 }
             }
             3. 修改节点状态 和 阻塞 (这里还可能会再次在for循环中再次获取锁)
             if (shouldParkAfterFailedAcquire(p, node) &&
                 parkAndCheckInterrupt())
                 throw new InterruptedException();
         }
     } finally {
         if (failed)
             cancelAcquire(node);
     }
 }

以上的实现都是AQS中的实现,只有tryAcquireShared方法是抽象的。需要在子类中实现。
在这里插入图片描述
这个方法会返回一个整数值,返回负值代表失败,零代表只有一个共享节点成功,而大于0的整数则预示后续的共享节点也可能获取锁成功。以下为官方注释
在这里插入图片描述
以下为tryAcquireShared方法在Semaphore中的公平锁和非公平锁的实现
在这里插入图片描述
可以看出,非公平锁和公平锁的基本实现是一模一样的,只是公平锁,先判断等待队列中是否已经有节点了,如果有节点,直接返回负值(失败),代表要进入等待队列,老老实实排队,不允许插队。

如果队列中没有节点或者非公平模式,获取状态值。这里的状态值就是构造Semaphorer时传入的permits值,也就是允许值。只要这个值大于1,线程就可以通过CAS操作获取锁,如果CAS成功,就会将这个值减1,返回剩余的许可值。直到值为0,这个方法就会返回-1,对应的线程就会在进入AQS的构造节点和进入等待队列的逻辑了。

final int nonfairTryAcquireShared(int acquires) {
   for (;;) {
       1. 获取许可值
       int available = getState();
       2. 减去当前的需求值 计算剩余值
       int remaining = available - acquires;
       3. 剩余值小于0 返回负值代表获取失败
          剩余值大于0 通过CAS修改状态为剩余值 修改成功 标识获取锁成功 如果CAS失败 进入下一次循环
       if (remaining < 0 ||
           compareAndSetState(available, remaining))
           return remaining;
   }
}

至于释放锁,首先大体逻辑同样是在AQS中实现的。

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

tryReleaseShared是抽象方法,由子类实现。如果返回false,代表整体释放失败,返回false。如果返回true,需要考虑从等待对列中唤醒下一个节点。

private void doReleaseShared() {
    /*
     * Ensure that a release propagates, even if there are other
     * in-progress acquires/releases.  This proceeds in the usual
     * way of trying to unparkSuccessor of head if it needs
     * signal. But if it does not, status is set to PROPAGATE to
     * ensure that upon release, propagation continues.
     * Additionally, we must loop in case a new node is added
     * while we are doing this. Also, unlike other uses of
     * unparkSuccessor, we need to know if CAS to reset status
     * fails, if so rechecking.
     */
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            头节点状态为SIGNAL就是一次唤醒后续节点
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

至于tryReleaseShared方法在Semaphorer中的公平与非公平实现都是一样的。通过无限循环和CAS保证将state值加释放值(releases)。

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");
        if (compareAndSetState(current, next))
            return true;
    }
}

https://www.processon.com/view/link/61950f6e5653bb30803c5bd2

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lang20150928

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值