-
Semaphore原理刨析
-
场景:
-
Semaphore信号量也是Java中的一个同步器,与CountDownLatch和CycleBarrier不同的是它内部的计数器是递增的,并且一开始初始化Semaphore时可以指定一个初始值,但是并不需要知道需要同步的线程个数,而是在需要同步的地方调用acquire方法时指定需要同步的线程个数。
代码实现如下:public class SemaphoreTest { private static volatile Semaphore semaphore = new Semaphore(0); public static void main(String[] args) throws InterruptedException { ExecutorService executorService = Executors.newFixedThreadPool(2); executorService.submit(new Runnable() { @Override public void run() { try { System.out.println(Thread.currentThread()+"A task over"); semaphore.release(); }catch (Exception e){ e.printStackTrace(); } } }); executorService.submit(new Runnable() { @Override public void run() { try { System.out.println(Thread.currentThread()+"A task over"); semaphore.release(); }catch (Exception e){ e.printStackTrace(); } } }); // ( 1 )等待子线程执行任务完毕,返回 semaphore.acquire(2); executorService.submit(new Runnable() { @Override public void run() { try { System.out.println(Thread.currentThread()+"B task over"); }catch (Exception e){ e.printStackTrace(); } } }); executorService.submit(new Runnable() { @Override public void run() { try { System.out.println(Thread.currentThread()+"B task over"); }catch (Exception e){ e.printStackTrace(); } } }); //(2) 等待子线程B执行完毕 semaphore.acquire(2); System.out.println("task over "); executorService.shutdown(); } }
在以上代码中将线程A和线程B加入到线程池,主线程执行代码(1)处后被阻塞,线程A和线程B调用release方法后信号量变为2,这时候主线程的acquire方法会获取到2个信号量后返回(返回后当前信号量为0).然后主线程条件线程C和线程D到线程池,然后主线程执行代码(2)被阻塞(因为主线程要获取两个信号量,而当前信号量个数为0).线程C和线程D执行完release方法后,主线程才返回。因此Semaphore在某种程度上实现了CyclicBarrier的复用功能。
-
-
原理:
由该类图可知,Semaphore还是使用AQS来实现的。Sync只是对AQS的一个修饰,Sync有两个实现类,用来指定获取信号量时采用的公平策略。public Semaphore(int permits) { sync = new NonfairSync(permits); } public Semaphore(int permits, boolean fair) { sync = fair ? new FairSync(permits) : new NonfairSync(permits); }
如上代码中Semaphore默认采用非公平策略。并且Semaphore的信号量的个数也是通过AQS中的state的值来实现的。
void acquire()
-
调用该方法是希望获取一个信号量资源。如果当前信号量个数大于0,则当前信号量个数会减1,然后该方法直接返回。如果当前信号量个数等于0,则当前线程会被放入AQS的阻塞队列中。如果其它线程调用了该线程的interrupt方法时,则会中断该线程然后抛出异常返回。
public void acquire() throws InterruptedException { //传递参数为1,说明要获取一个信号量资源 sync.acquireSharedInterruptibly(1); } public final void acquireSharedInterruptibly(int arg) throws InterruptedException { //如果线程被中断则抛出异常 if (Thread.interrupted()) throw new InterruptedException(); //否则调用Sync子类尝试获取。 if (tryAcquireShared(arg) < 0) //如果调用失败则放入阻塞队列,然后再次尝试 doAcquireSharedInterruptibly(arg); }
在以上代码中acquire在内部调用Sync的acquireSharedlnterruptibly方法,后者对中断进行相应。尝试获取信号量资源的AQS的方法tryAcquireShared是由Sync的子类实现的,所以这里分别从两方面来讨论。
final int nonfairTryAcquireShared(int acquires) { for (;;) { //获取当前信号量值 int available = getState(); //计算当前剩余值 int remaining = available - acquires; //如果当前剩余值小于0或者CAS设置成功返回 if (remaining < 0 || compareAndSetState(available, remaining)) return remaining; } }
如上代码中先获取当前信号量值(available),然后减去需要获取的值(acquires),得到剩余的信号量个数(remaining),如果剩余值小于0则说明当前信号量个数满足不了需求,那么直接返回负数,这时当前线程会被放入AQS的阻塞队列而被挂起。如果剩余值大于0,则使用CAS操作设置当前信号量为剩余值,然后返回剩余值。
以上是非公平的逻辑,公平逻辑如下: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; } }
-
在公平策略中会通过hasQueuedPredecessors函数去AQS队列中查询是否有头结点的线程,如果有则优先获取。
-
void acquire(int permits)
-
该方法中与acquire方法不同,acquire只需要获取一个信号量,acquire(int permits)需要获取permits个。
public void acquire(int permits) throws InterruptedException { if (permits < 0) throw new IllegalArgumentException(); sync.acquireSharedInterruptibly(permits); }
-
-
void acquireUninterruptibly()
- 该方法与aquire () 似,不同之处在于该方法 对中断不响应,也就是当当前线程调用了 acquireUninterruptibly 获取资源时(包含被阻塞后 ),其他线程调用了当前线程的 interrupt ( )方法设置了当前线程的中断标志,此时当前线程并不会抛出
IntrruptException 异常而返回。
- 该方法与aquire () 似,不同之处在于该方法 对中断不响应,也就是当当前线程调用了 acquireUninterruptibly 获取资源时(包含被阻塞后 ),其他线程调用了当前线程的 interrupt ( )方法设置了当前线程的中断标志,此时当前线程并不会抛出
-
void release()
- 该方法的作用是把当前Semaphore对象的信号量加1,如果当前有线程调用acquire方法而被阻塞放入AQS阻塞队列中,则会根据公平策略选择一个信号量个数能被满足的线程进行激活,激活的线程会尝试获取刚增加的信号量。
public void release() { sync.releaseShared(1); } public final boolean releaseShared(int arg) { //尝试释放资源 if (tryReleaseShared(arg)) { //资源释放成功则调用park方法唤醒AQS中先挂起的线程 doReleaseShared(); return true; } return false; } protected final boolean tryReleaseShared(int releases) { for (;;) { //获取当前信号量 int current = getState(); //将当前信号量增加release int next = current + releases; if (next < current) // overflow throw new Error("Maximum permit count exceeded"); //使用CAS保证更新信号量值的原子性 if (compareAndSetState(current, next)) return true; } }
release()->sync.releaseShared(1)可知,release方法每次只对信号量增加1,tryReleaseShared方法是无限循环,使用CAS保证release方法对信号量递增1的原子性操作。tryReleaseShared方法增加信号量值成功后会调用AQS的方法来激活因为调用acquire方法而被阻塞的线程。
-
总结:
- Semaphore的计数器是不可以自动重置的,不过通过变相的改变acquire方法的参数可以实现CycleBarrier的功能。Semaphore也是通过AQS实现的,并且获取信号量是分为公平和非公平策略。
-
《Java后端知识体系》系列之Semaphore的原理剖析
最新推荐文章于 2023-12-05 14:04:04 发布