Java多线程系列--“JUC锁”11之 Semaphore信号量的原理

一、Semaphore简介

Semaphore是一个计数信号量,它的本质是一个"共享锁",是基于AQS实现的,通过state变量来实现共享。通过调用acquire方法,对state值减去一,当调用release的时候,对state值加一。当state变量小于0的时候,在AQS队列中阻塞等待。

信号量维护了一个信号量许可集。线程可以通过调用acquire()来获取信号量的许可;当信号量中有可用的许可时,线程能获取该许可;否则线程必须等待,直到有可用的许可为止。 线程可以通过release()来释放它所持有的信号量许可。

 

更多的semaphore介绍见《Java 信号量 Semaphore 入门介绍》,本文从源码层面介绍一下semaphore原理。

二、Semaphore数据结构

Semaphore的UML类图如下:

从图中可以看出:
(01) Semaphore实现的思路跟ReentrantLock非常的相似,包括内部类的结构都是一样的,也是有公平和非公平两种模式。只是不同的是Semaphore是共享锁,支持多个线程同时操作;然而ReentrantLock是互斥锁,同一个时刻只允许一个线程操作。
(02) Sync包括两个子类:"公平信号量"FairSync 和 "非公平信号量"NonfairSync。sync是"FairSync的实例",或者"NonfairSync的实例";默认情况下,sync是NonfairSync(即,默认是非公平信号量)。同样的Sync是一个继承于AQS的抽象类。

 

 

 

 

示例:

public class SemaphoreDemo {
    // 创建一个有2个收费口的收费站
    private static Semaphore semaphore = new Semaphore(2);

    public static class RunThread extends Thread {
        @Override
        public void run() {
            // 这里循环100次,模拟车辆非常多,竞争激烈
            for (int i = 0; i < 100; i++) {
                doBusiness();
            }
        }
        // 这里模拟通过收费口的情况,业务操作
        private void doBusiness() {
            try {
                // 获取信号
                semaphore.acquire();
                // 模拟业务操作耗时
                Thread.sleep(2000);
                // 打印信息
                System.out.println(Thread.currentThread().getName() + "获取信号");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                // 释放信号
                semaphore.release();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 创建4个线程
        RunThread runThread1 = new RunThread();
        RunThread runThread2 = new RunThread();
        RunThread runThread3 = new RunThread();
        RunThread runThread4 = new RunThread();
        // 启动线程
        runThread1.start();
        runThread2.start();
        runThread3.start();
        runThread4.start();

        // 主线程等待runThread1、2、3、4结束之后再往下运行
        runThread1.join();
        runThread2.join();
        runThread3.join();
        runThread4.join();

        System.out.println("结束");
    }

运行程序,你会发现每次打印只会打印2条日志,也就是时候每次最多只会有2辆车同时经过收费站。

 

 

 

三、源码分析

Semaphore源码分析(基于JDK1.8)

在《Java 信号量 Semaphore 入门介绍》的示例里,创建了一个拥有5个许可证的信号量,代码片段如下:

// 初始化信号量,个数为 5
private static Semaphore s = new Semaphore(5);

3.1、非公平信号量

我们看一下构造器:

    public Semaphore(int permits) {
        sync = new NonfairSync(permits);
    }

从构造器里面可以看出来semaphore默认实现的是非公平锁,我们在看一下NonfairSync类,它是Semaphore的内部类:java.util.concurrent.Semaphore$NonfairSync.java 

3.1.1、非公平信号量 类源码

    /**
     * NonFair version
     */
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = -2694183684443567898L;

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

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

我们可以看到NonfairSync类继承了Sync,而Sync继承了AQS,从这里其实可以看出来semaphore是基于AQS实现的。

 

 3.2、公平信号量

创建一个拥有5个许可证的信号量,代码片段如下:

    // 初始化信号量,个数为 5
    private static Semaphore s = new Semaphore(5, true);

 

构造器:

    public Semaphore(int permits, boolean fair) {
        sync = fair ? new FairSync(permits) : new NonfairSync(permits);
    }

 

 3.2.1、公平信号量 源码

    static final class FairSync extends Sync {
        private static final long serialVersionUID = 2014338818796000944L;

        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;
            }
        }
    }

我们可以看到FairSync类继承了Sync,而Sync继承了AQS,从这里其实可以看出来semaphore是基于AQS实现的。

3.2.2、 公平信号量获取

Semaphore中的公平信号量是FairSync。它的获取API如下:

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

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

Semaphore.acquire方法源码直接是调用FairSync的acquireSharedInterruptibly,也就是进入了AQS的acquireSharedInterruptibly模板方法里面,之前我们就讲过了。

acquireSharedInterruptibly()的源码如下:

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

这个方法定义了一个模板流程:

第一步:是先调用子类的tryAcquireShared方法获取共享锁,也就是获取信号量。
第二步:如果获取信号量成功,即返回值大于等于0,则直接返回。
第三部:如果获取失败,返回值小于0,则调用AQS的doAcquireSharedInterruptibly方法,进入AQS的等待队列里面,等待别人释放资源之后它再去获取。
这里的流程就可以得到一个如下的图:

doAcquireSharedInterruptibly方法的流程我们之前讲解AQS的时候都完全讲解过了,所以只需要分析一下FairSync子类的tryAcquireShared方法的内部源码即可:

protected int tryAcquireShared(int acquires) {
    for (;;) {
        // 这里作为公平模式,首先判断一下AQS等待队列里面
        // 有没有人在等待获取信号量,如果有人排队了,自己就不去获取了
        if (hasQueuedPredecessors())
            return -1;
        // 获取剩余的信号量资源
        int available = getState();
        // 剩余资源减去我需要的资源,是否小于0
        // 如果小于0则说明资源不够了
        // 如果大于等于0,说明资源是足够我使用的
        int remaining = available - acquires;
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

上面的源码就是获取信号量的核心流程了:
(1)首先判断一下AQS等待队列里面是否有人在排队,如果是,则自己不尝试获取资源了,乖乖的去排队
(2)如果没有人在排队,获取一下当前剩余的信号量available,然后减去自己需要的信号量acquires,得到减去后的结果remaining。
(3)如果remaining小于0,直接返回remaining,说明资源不够,获取失败了,这个时候就会进入AQS等待队列等待。
(4)如果remaining 大于等于0,则执行CAS操作compareAndSetState竞争资源,如果成功了,说明自己获取信号量成功,如果失败了同样进入AQS等待队列。

这里画一下公平模式FairSync的tryAcquireShared流程图,以及整个公平模式的acquire方法的流程图:

 

 上面就是我分析得到的FairSync公平模式的acquire获取信号量的全部流程图了。

AQS的hasQueuedPredecessors():
    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());
    }

 

说明:tryAcquireShared()的作用是尝试获取acquires个信号量许可数。
对于Semaphore而言,state表示的是“当前可获得的信号量许可数”。

3.2.3、 公平信号量的释放

Semaphore中公平信号量(FairSync)的释放API如下:

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

public void release(int permits) {
    if (permits < 0) throw new IllegalArgumentException();
    sync.releaseShared(permits);
}

 我们继续来分析releaseShared方法,进入到AQS的releaseShard释放资源的模板方法

public final boolean releaseShared(int arg) {
    // 1. 调用子类的tryReleaseShared释放资源
    if (tryReleaseShared(arg)) {
        // 释放资源成功,调用doReleaseShared唤醒等待队列中等待资源的线程
        doReleaseShared();
        return true;
    }
    return false;
}

 

其它和非公平信号量的释放相同。

 这里的模板流程有:
(1)调用子类的tryReleaseShared去释放资源,即释放信号量
(2)如果释放成功了,则调用doReleaseShared唤醒AQS中等待资源的线程,将资源传播下去,如果释放失败,即返回小于等于0,则直接返回。
所以,这里除了AQS的核心模板流程之外,具体释放逻辑就是Sync的tryReleaseShared方法的源码了,我们继续来查看:

 Sync的tryReleaseShard源码

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设置资源,成功直接返回,失败则进入下一循环重试
        if (compareAndSetState(current, next))
            return true;
    }
}

这里的逻辑非常简单了,无非是不断尝试CAS将资源加回去而已。


针对Semaphore释放资源的流程,我这边也画了一副图出来:

 

 

 非公平模式NonfairSync跟公平模式唯一的区别就是在tryAcquireShared上的实现不一样,其它的完全都是一致的,我们下面就看一下NonfairSync的tryAcquireShared方法源码:

 NonfairSync的tryAcquireShared方法源码:

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

这里是直接调用了Sync的nonfairTryAcquireShared方法源码,我们继续往下看:

final int nonfairTryAcquireShared(int acquires) {
    for (;;) {
        int available = getState();
        // 上面公平模式需要看下等待队列是否有人
        // 这里是直接去尝试获取资源啊,根本不管是否有人
        int remaining = available - acquires;
        if (remaining < 0 ||
            // 如果remaining剩余资源 >= 0 则执行CAS操作
            compareAndSetState(available, remaining))
            return remaining;
    }
}

这里非公平锁的源码流程就非常简单了:
(1)对比上面的公平模式,需要判断AQS等待队列是否有人在等待。而这里非公平模式不管有没有人在等
(2)如果剩余可用资源remaining >= 0,则直接CAS去争抢资源,成功则返回,失败则重试。



四、总结

1、"公平信号量"和"非公平信号量"的区别

"公平信号量"和"非公平信号量"的释放信号量的机制是一样的!不同的是它们获取信号量的机制:线程在尝试获取信号量许可时,对于公平信号量而言,如果当前线程不在CLH队列的头部,则排队等候;而对于非公平信号量而言,无论当前线程是不是在CLH队列的头部,它都会直接获取信号量。该差异具体的体现在,它们的tryAcquireShared()函数的实现不同。

2、一般而言,非公平时候的吞吐量要高于公平锁”,这是为什么呢?

非公平锁性能高于公平锁性能的原因:在恢复一个被挂起的线程与该线程真正运行之间存在着严重的延迟。假设线程A持有一个锁,并且线程B请求这个锁。由于锁被A持有,因此B将被挂起。当A释放锁时,B将被唤醒,因此B会再次尝试获取这个锁。与此同时,如果线程C也请求这个锁,那么C很可能会在B被完全唤醒之前获得、使用以及释放这个锁。这样就是一种双赢的局面:B获得锁的时刻并没有推迟,C更早的获得了锁,并且吞吐量也提高了。当持有锁的时间相对较长或者请求锁的平均时间间隔较长,应该使用公平锁。在这些情况下,插队带来的吞吐量提升(当锁处于可用状态时,线程却还处于被唤醒的过程中)可能不会出现。

3、Semaphore与jdk中的Lock的区别

1. 使用Lock.unlock()之前,该线程必须事先持有这个锁(通过Lock.lock()获取),如下:

public class LockTest {
    public static void main(String[] args) {
        Lock lock=new ReentrantLock();
        lock.unlock();
    }
}

则会抛出异常,因为该线程事先并没有获取lock对象的锁:

Exception in thread "main" java.lang.IllegalMonitorStateException
    at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:155)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1260)
    at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:460)
    at LockTest.main(LockTest.java:12)

对于Semaphore来讲,如下:

public class SemaphoreTest {    
    public static void main(String[] args) {
        Semaphore semaphore=new Semaphore(1);//总共有1个许可
        System.out.println("可用的许可数目为:"+semaphore.availablePermits());
        semaphore.release();
        System.out.println("可用的许可数目为:"+semaphore.availablePermits());
    }
}

结果如下:

可用的许可数目为:1
可用的许可数目为:2

i. 并没有抛出异常,也就是线程在调用release()之前并不要求先调用acquire() 
ii. 我们看到可用的许可数目增加了一个,但我们的初衷是保证只有一个许可来达到互斥排他锁的目的,所以这里要注意一下

参考:https://mp.weixin.qq.com/s/-wlEUriTNRSN7V-UxsFm6g

posted on 2016-11-14 22:15  duanxz  阅读(1043)  评论(0编辑  收藏  举报