JDK源码系列 Semaphore源码分析

前言

Semaphore(信号量),是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。

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

一、Semaphore整体结构

在这里插入图片描述

二、内部类Sync

Semaphore内部实现了Sync抽象类,而且还有公平锁和非公平锁区分,这和ReentrantLock十分相似。我们来看一下。

abstract static class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = 1192457210091910933L;
	
	/*设置Sync的初始状态*/
    Sync(int permits) {
        setState(permits);
    }

	/*获取Sync当前的状态*/
    final int getPermits() {
        return getState();
    }

	/*
     * 非公平锁尝试获取资源
	 * 我们可以看到Sync内部使用的锁结构是共享锁
	 */
    final int nonfairTryAcquireShared(int acquires) {
        for (;;) {
        	/*获取锁的状态*/
            int available = getState();
            int remaining = available - acquires;
            /*
            	1.如果state小于0,表示没有资源,直接返回
             	2.如果cas设置成功,表示获取资源成功,直接返回;若cas失败,会自旋直至设置成功或者没有资源
             */
            if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                return remaining;
        }
    }
	
	/*释放共享锁资源*/
    protected final boolean tryReleaseShared(int releases) {
        for (;;) {
        	/*获取锁的状态*/
            int current = getState();
            int next = current + releases;
            /*permit的最大值是Integer.MAX_VALUE,如果超过该上限的话,说明到达了最大许可数,直接抛出异常*/
            if (next < current) // overflow
                throw new Error("Maximum permit count exceeded");
            /*通过cas自旋直至设置成功*/
            if (compareAndSetState(current, next))
                return true;
        }
    }
	
	/*减少许可的数量*/
    final void reducePermits(int reductions) {
        for (;;) {
        	/*获取状态*/
            int current = getState();
            int next = current - reductions;
            /*许可数量超过下限*/
            if (next > current) // underflow
                throw new Error("Permit count underflow");
            /*通过cas更新锁的状态值*/
            if (compareAndSetState(current, next))
                return;
        }
    }
    
	/*获取剩余全部的许可*/
    final int drainPermits() {
        for (;;) {
            int current = getState();
            /*cas将当前状态设置为0*/
            if (current == 0 || compareAndSetState(current, 0))
                return current;
        }
    }
}

Sync内部类结构很简单,函数也很容易理解,它继承了AQS,主要实现了非公平锁获取锁的函数以及释放锁的函数。由于这些函数基本没有比较难以理解的点,这里我们就不在赘述。不过还有一点值得注意的是,ReentrantLock内部Sync的实现是利用AQS的独占锁,而Semaphore内部Sync的实现是利用AQS的共享锁。

2.1 NofairSync

非公平锁的实现:

static final class NonfairSync extends Sync {
    private static final long serialVersionUID = -2694183684443567898L;

    NonfairSync(int permits) {
        super(permits);
    }
	
	/*调用Sync类的nonfairTryAcquireShared方法*/
    protected int tryAcquireShared(int acquires) {
        return nonfairTryAcquireShared(acquires);
    }
}
2.2 FairSync

公平锁的实现:

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

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

    protected int tryAcquireShared(int acquires) {
        for (;;) {
        	/*判断CLH同步队列中是否有正在等待获取锁的线程 如果有,直接返回-1*/
            if (hasQueuedPredecessors())
                return -1;
            /*如果CLH队列中没有等待获取的线程,那么执行下面的步骤,基本和非公平锁的不走差不多*/
            int available = getState();
            int remaining = available - acquires;
            if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                return remaining;
        }
    }
}
2.3 公平锁和非公平锁的差异

我们可以看到,Semaphore非公平锁和公平锁就多了一步判断,这步判断就是利用hasQueuedPredecessors()判断CLH同步队列中是否有正在等到获取锁的线程,如果有直接返回-1,如果没有,那么当前线程就可以尝试去获取资源。
其实这里和ReentrantLock是十分相似的,至于hasQueuedPredecessors()的分析,可以查看JDK源码系列 ReentrantLock 公平锁和非公平锁的实现原理,这里就不再赘述。


讲完Semaphore的锁的实现,我们来看一下Semaphore的主要方法。

三、Semaphore主要方法
3.1 构造方法
/*该构造方法默认使用的是非公平锁,创建一个Semaphore实例并设置许可的数量*/
public Semaphore(int permits) {
    sync = new Semaphore.NonfairSync(permits);
}
/*该构造方法可以指定Semaphore使用的锁类型*/
public Semaphore(int permits, boolean fair) {
    sync = fair ? new Semaphore.FairSync(permits) : new Semaphore.NonfairSync(permits);
}
3.2 acquire

当前函数可以从Semaphore获取许可(默认获取一个许可),如果获取许可成功,将继续执行,若获取许可失败,当前线程将被park进入waiting状态。

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

我们可以看到acquire其实调用的是AQS的acquireSharedInterruptibly((int arg)方法,该方法和acquireShared((int arg)类似,只是该方法对中断是敏感的,会响应中断。
具体可以看JDK源码系列 AQS续篇共享锁源码实现分析中关于共享锁acquireShared((int arg)方法的解析,我们这里不再赘述。

当线程获取到许可时,即获取到了共享锁,那么该线程可以继续往下执行。若线程没有获取到许可,那么该线程会以共享结点的形式加入到CLH同步队列中,等待获取锁。

acquire(int permits)是从Semaphore中获取指定数量的许可。

至于acquireUninterruptibly()acquireUninterruptibly(int permits)这两个方法和上面讲的相似,只是这两个方法不会抛出中断异常,它们会将中断(中断标志为true)外抛,由我们自己决定如何去处理中断。

3.3 tryAcquire

调用该方法可以尝试去获取许可。

public boolean tryAcquire() {
	/*调用非公平锁的尝试获取许可(默认1个)的方法*/
	return sync.nonfairTryAcquireShared(1) >= 0;
}

/*尝试在指定时长内去获取许可*/
public boolean tryAcquire(long timeout, TimeUnit unit)
        throws InterruptedException {
    return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
//还有两个tryAcquire是获取指定许可,这里就不列出来了

tryAcquire可以让当前线程尝试去获取许可。
tryAcquireSharedNanos方法是在AQS的方法,作用是在指定时长内去尝试获取锁,这里不多说。

3.4 release

调用该方法可以释放许可(释放共享锁)并唤醒CLH队列中的线程来尝试获取许可(共享锁)。

public void release() {
	/*调用AQS的releaseShared去释放资源*/
    sync.releaseShared(1);
}

/*释放指定许可*/
public void release(int permits) {
	if (permits < 0) throw new IllegalArgumentException();
       sync.releaseShared(permits);
}

release方法很简单,就是释放当前线程所持有许可(共享锁),并唤醒CLH队列中的线程去尝试获取锁资源。

3.4 其余方法
	/*当前可用的许可数*/
    public int availablePermits() {
        return sync.getPermits();
    }
   	/*获取当前Semaphore剩余全部的许可数量*/
    public int drainPermits() {
        return sync.drainPermits();
    }
 	/*减少许可的数量*/
    protected void reducePermits(int reduction) {
        if (reduction < 0) throw new IllegalArgumentException();
        sync.reducePermits(reduction);
    }
    /*判断CLH队列是否还有线程在等待获取锁*/
    public final boolean hasQueuedThreads() {
        return sync.hasQueuedThreads();
    }
    /*获取CLH队列的结点的数量*/
    public final int getQueueLength() {
        return sync.getQueueLength();
    }

以上,便是Semaphore的全部源码分析,我们可以看到,Semaphore其实是利用AQS的共享锁模式来控制同时访问特定资源的线程数量。

接下来我们来看一下相关应用:

public class SemaphoreTest {
    private static final int THREAD_NUM = 30;
    private static ThreadPoolExecutor threadPool = (ThreadPoolExecutor) Executors.newFixedThreadPool(THREAD_NUM);
    private static Semaphore semaphore = new Semaphore(10);   
    static {
        threadPool.prestartAllCoreThreads();
    }    
    public static void main(String[] args) {
        try {
            for (int i = 0; i < THREAD_NUM; i++) {
                threadPool.execute(()->{
                    try {
                        semaphore.acquire();
                        System.out.println("running");
                        Thread.sleep(1000);
                        semaphore.release();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                });
            }
        }finally {
            threadPool.shutdown();
        }
    }
}

在以上demo中,我们设置了Semaphore的许可数量为10。而在线程池有30个线程在运行,但是每次只允许10个线程并发执行。当当前线程调用acquire获取许可成功并执行完毕,随后调用release来释放许可,唤醒在CLH队列中的线程来获取许可(基本上是每次10个线程在并发运行)。

3.5 应用场景

从上面的例子我们已经明白了Semaphore的运行机制,通过该运行机制我们可以利用Semaphore来做流量控制,特别是公共资源有限的应用场景,比如数据库连接。假如有一个需求,要读取几万个文件的数据,因为都是IO 密集型任务 ,我们可以启动几十个线程并发地读取,但是如果读到内存后,还需要存储到数据库中,而数据库的连接数只有10个, 这时我们必须控制只有10个线程同时获取数据库连接保存数据,否则会报错无法获取数据库连接。这个时候,就可以使用 Semaphore 来做流量控制。 --[摘自Java并发编程的艺术]


至此,关于Semaphore的全部内容已经分析完毕。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值