文章目录

  • 一、CountDownLatch
  • 1、使用
  • 2、类图
  • 3、实现原理探究
  • (1) void await() 方法
  • (2) await(long timeout, TimeUnit unit) 方法
  • (3) void countDown() 方法
  • (4) getCount() 方法
  • 二、回环屏障 CyclicBarrier
  • 1、使用
  • 2、类图
  • 3、实现原理探究
  • (1) int await() 方法
  • (2) await(long timeout, TimeUnit unit) 方法
  • (3) int dowait(boolean timed, long nanos) 方法
  • 三、 信号量 Semaphore
  • 1、使用
  • 2、类图
  • 3、实现原理
  • (1) void acquire() 方法
  • (2) void acquire(int permits) 方法
  • (3)acquireUninterruptibly() 方法
  • (4) acquireUninterruptibly(int permits) 方法
  • (5) void release() 方法
  • (6)release(int permits)
  • 四、总结


一、CountDownLatch

    在日常开发中,可能会遇到 需要在主线程中 开启多个线程 去执行任务,并且 主线程需要等待 所有子线程执行完毕后 再进行汇总 的情景。在 CountDownLatch 出现之前,一般都使用线程的 join() 方法来实现这一点,但是 join 不够灵活,不能够满足 不同场景的要求。

1、使用

👀 : CountDownLatch 的使用

package SYNC.CountDownLatchPack;

import java.util.concurrent.CountDownLatch;

public class JoinCountDownLatch {

    // 创建一个 CountDownLatch 实例
    private static volatile CountDownLatch  countDownLatch = new CountDownLatch(2);

    public static void main(String[] args) throws InterruptedException {
        Thread threadOne = new Thread(new Runnable() {
            @Override
            public void run() {
                try{
                    Thread.sleep(1000);
                    System.out.println("child threadOne over!");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    countDownLatch.countDown();
                }
            }
        });

        Thread threadTwo = new Thread(new Runnable() {
            @Override
            public void run() {
                try{
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    countDownLatch.countDown();
                }
            }
        });

        // 启动子线程
        threadOne.start();
        threadTwo.start();
        System.out.println("wait all child thread over!");

        // 等待子线程完毕后返回
        countDownLatch.await();

        System.out.println("all child thread over!");
    }
}

运行结果:

java wms第三方同步池实现方式_Java并发同步器

    例子中创建了一个 CountDownLatch 的实例,因为有两个子线程,所以 构造方法里 传入的参数 是 2。主方法里调用 countDownLatch.await();后会被阻塞。子线程调用 countDownLatch.countDown(); 方法,会使 CountDownLatch 内部的计数器减 1,两个子线程执行完毕后,计数器值会变为 0,这时 主线程的 await() 方法 才会返回。
    其实以上代码不够优雅,在项目实践中 一般都避免直接操作线程,而是使用 ExecutorService 线程池来管理。 使用 ExecutorService 时传递的参数是 Runable 或者 Callable 对象,这时没办法直接调用线程的 join 方法,就需要选择使用 CountDownLatch 了。修改代码为:

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class JoinCountDownLatch {
/*
    // 创建一个 CountDownLatch 实例
    private static volatile CountDownLatch  countDownLatch = new CountDownLatch(2);

    public static void main(String[] args) throws InterruptedException {
        Thread threadOne = new Thread(new Runnable() {
            @Override
            public void run() {
                try{
                    Thread.sleep(1000);
                    System.out.println("child threadOne over!");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    countDownLatch.countDown();
                }
            }
        });

        Thread threadTwo = new Thread(new Runnable() {
            @Override
            public void run() {
                try{
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    countDownLatch.countDown();
                }
            }
        });

        // 启动子线程
        threadOne.start();
        threadTwo.start();
        System.out.println("wait all child thread over!");

        // 等待子线程完毕后返回
        countDownLatch.await();

        System.out.println("all child thread over!");
    }
    */
    // 创建一个 CountDownLatch 实例
    private static CountDownLatch  countDownLatch = new CountDownLatch(2);

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        
        // 将线程 A 添加到线程池
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                try{
                    Thread.sleep(1000);
                    System.out.println("child threadOne over!");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    countDownLatch.countDown();
                }
            }
        });

        // 将线程 B 添加到线程池
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                try{
                    Thread.sleep(1000);
                    System.out.println("child threadTwo over!");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    countDownLatch.countDown();
                }
            }
        });
        
        System.out.println("wait all child thread over!");
        
        // 等待子线程执行完毕,返回
        countDownLatch.await();
        System.out.println("all child thread over!");
        executorService.shutdown();
    }
}

🙌join 方法 与 CountDownLatch 方法的区别
    调用一个子线程的 join() 方法后,该线程会一直阻塞 直到子线程运行完毕,而 CountDownLatch 则使用计数器来允许子线程运行完毕 或者 在运行中递减计数,也就是说,CountDownLatch 可以在子线程运行的任何时候让 await 方法返回,而不一定必须等到线程结束。
    另外,使用线程池来管理线程时,一般都是直接添加 Runable 到线程池,这时候就没办法再调用线程的 join 方法啦, CountDownLatch 比 join 发 让我们对线程同步有更灵活的控制。
    

2、类图

java wms第三方同步池实现方式_Semaphore_02

构造方法:

public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");

		// (一)
        this.sync = new Sync(count);
    }

(一):

Sync(int count) {
            setState(count);
        }

    可以看到, CountDownLatch 的内部有个计数器,并且这个计数器是递减的。 CountDownLatch 是使用 AQS 实现的,构造方法中传入 count 值,实际上是把 计数器的值 count 赋给了 AQS 的状态变量 state,也就是说, 使用 AQS 的 状态值来表示计数器值
    

3、实现原理探究
(1) void await() 方法

    当线程调用 CountDownLatch 的 await 方法后,当前线程会被阻塞,直到以下情况才会返回:

  • 当所有线程都调用了 CountDownLatch 对象的 countDown 方法后,也就是 计数器的值为 0 时。
  • 其他线程调用了当前线程的 interrupt() 方法 中断了当前线程,当前线程会抛出 InterruptedException 异常,然后返回。
    源码:
public void await() throws InterruptedException {
		// (一)
        sync.acquireSharedInterruptibly(1);
    }

(一)acquireSharedInterruptibly:

public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
            
        // 如果线程被中断,则 抛出异常
        if (Thread.interrupted())
            throw new InterruptedException();

		// (二) 如果当前计数值不为 0,则 进入 AQS 的队列等待
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }

(二):

protected int tryAcquireShared(int acquires) {
  		// 查看当前状态值 即 计数器值
        return (getState() == 0) ? 1 : -1;
    }

    可以看到,void await() 方法 的特点是 线程获取资源时 可以被中断加粗样式,并且 获取的资源是 共享资源。而且,tryAcquireShared 方法中传入的 arg 参数并没有被用到,调用该方法仅仅是检查当前状态值是不是 0,并没有调用 CAS 让当前状态值 -1。
    

(2) await(long timeout, TimeUnit unit) 方法

    当线程调用 CountDownLatch 的 await 方法后,当前线程会被阻塞,直到以下情况才会返回:

  • 当所有线程都调用了 CountDownLatch 对象的 countDown 方法后,也就是 计数器的值为 0 时。
  • 其他线程调用了当前线程的 interrupt() 方法 中断了当前线程,当前线程会抛出 InterruptedException 异常,然后返回。
  • 设置的 timeout 时间到了,因为超时而返回 false。

源码:

public boolean await(long timeout, TimeUnit unit)
        throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }

    

(3) void countDown() 方法

    线程调用该方法后,计数器的值 递减,递减后,如果计数器的值为 0 则唤醒所有 因 调用 await 方法 而被阻塞的线程,否则 什么也不做。
源码:

public void countDown() {
 
 		// (一)
        sync.releaseShared(1);
    }

(一):

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

(二):

protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
            for (;;) {
            	// 获取当前状态值 即 计数器值
                int c = getState();

                if (c == 0)
                    return false;
                    
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }

    可以看到,在循环中,首先获取当前状态值,即 计数器的值,如果状态值为 0 ,则返回 false,那么 countDown() 方法也返回。否则 是固态 CAS 将 计数器值 减1,CAS 失败则 循环重试,如果 CAS 成功了,再看 当前计数器值是否为 0,为 0 则返回 true,说明最后一个线程调用了 countDown 方法,那么,该线程除了让计数器值 减1 以外,还需要唤醒因为调用了 CountDownLatch 的 await 方法而被阻塞的线程。doReleaseShared(); 方法会去激活阻塞的线程。
    

(4) getCount() 方法

     获取当前计数器的值,也就是 AQS 的 state 的值。

源码:

public long getCount() {
        return sync.getCount();
    }

    
🎭 总结:
     相比于 join,CountDownLatch 方法对线程同步有更灵活的控制,它是使用 AQS 实现的,使用 AQS 的状态值来存放计数器的值。首先在初始化 CountDownLatch 时 设置状态值(计数器值),当多个线程调用 countDown() 方法时 实际是原子性递减 AQS 的状态值。当线程调用 await() 方法后 当前线程会被放入 AQS 的阻塞队列,等待计数器为 0 再返回。其他线程调用 countdown 方法让计数器减 1 ,当计数器变为 0 ,当前线程会调用 AQS 的 doReleaseShared 方法 来激活 由于调用 await() 方法而被阻塞的线程。
    

二、回环屏障 CyclicBarrier

     上述 CountDownLatch 的计数器是一次性的,也就是说,等到计数器值变为 0 后,再调用 CountDownLatch 的 await 和 countdown 方法都会立刻返回,这时就起不到线程同步的作用了。为了满足 计数器可以重置 的需要,提供了 CyclicBarrier。 它可以让一组线程全部达到一个状态后 再全部同时执行 。之所以叫 “回环”,是因为 当所有等待线程执行完毕,并重置 CyclicBarrier 的状态后,它可以被重用。之所以叫 “屏障”,是因为 线程调用 await 方法后就会被阻塞,这个阻塞点就叫 屏障点 ,等所有线程都调用了 await 方法后,线程们就会冲破屏障,继续向下执行。

1、使用

例 1👀:使用两个线程去执行一个被分解的任务 A,当两个线程把自己的任务都执行完毕后,再对它们的结果汇总处理。

import java.util.concurrent.*;

public class CyclicBarrierTest1 {
    // 创建一个 CyclicBarrier 实例,添加一个 所有子线程 全部达到屏障后 执行的任务
    private static CyclicBarrier cyclicBarrier = new CyclicBarrier(2, new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread() + "task1 merge result");
        }
    });

    public static void main(String[] args) {
        // 创建一个线程个数固定为 2 的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        // 将线程 A 添加到线程池
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                try{
                    System.out.println(Thread.currentThread()+ "task1-1");
                    System.out.println(Thread.currentThread()+"enter in barrier");

                    cyclicBarrier.await();

                    System.out.println("enter to barrier");
                } catch (Exception e) {
              e.printStackTrace();
                }
            }
        });
        // 将线程 B 添加到线程池
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                try{
                    System.out.println(Thread.currentThread()+ "task1-2");
                    System.out.println(Thread.currentThread()+"enter in barrier");

                    cyclicBarrier.await();

                    System.out.println("enter to barrier");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });

        // 关闭线程池
        executorService.shutdown();
    }
}

运行结果:

java wms第三方同步池实现方式_java wms第三方同步池实现方式_03


    可以看到,创建了一个 CyclicBarrier 对象,第一个参数是 计数器初始值,第二个参数是 当计数器值为 0 时 需要执行的任务。在 main 方法中,首先创建了一个大小为 2 的线程池,然后添加两个子任务到线程池,每个任务在执行完自己的逻辑后,会调用 cyclicBarrier.await(); 。一开始,计数器值为 2,当 第一个线程调用 await 方法后,计数器值会递减为 1,这时由于计数器不为 0 ,所以当前线程 就到了屏障点, 而被阻塞。然后,第二个线程 调用 await 时,会进入屏障,计数器值也会递减,计数器变为 0 ,这时就会去执行 CyclicBarrier 构造方法里的任务,执行完毕后退出屏障点,并且唤醒被阻塞的第二个线程,第一个线程也会退出屏障点,继续向下运行。

    这个例子说明,多个线程之间是互相等待的,假如 计数器值为 N,那么 随后调用 await 方法的 N-1 个线程 都会因为到达屏障点而被阻塞,当 第 N 个线程调用 await 方法后,计数器值变为 0 了,这时,第 N 个线程 才会发出通知,唤醒前面的 N-1 个线程。也就是 当全部线程到达屏障点时,才能一起继续向下执行。

例 2👀:CyclicBarrier 的可重用性
    假设,一个任务由 阶段 1、阶段 2 和 阶段 3 组成,每个线程要串行地执行阶段 1、阶段 2 和 阶段 3,当多个线程执行该任务时,必须要保证所有线程的阶段 1 全部完成后 才能进入阶段 2,当所有线程的阶段 2 全部完成后,才能进入阶段 3 执行。

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

public class CyclicBarrierTest2 {

    // 创建一个 CyclicBarrier 实例
    private static CyclicBarrier cyclicBarrier = new CyclicBarrier(2);

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        // 将线程 A 添加到线程池
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println(Thread.currentThread() + "step1");
                    cyclicBarrier.await();

                    System.out.println(Thread.currentThread() + "step2");
                    cyclicBarrier.await();

                    System.out.println(Thread.currentThread() + "step3");
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        });

        // 将线程 B 添加到线程池
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println(Thread.currentThread() + "step1");
                    cyclicBarrier.await();

                    System.out.println(Thread.currentThread() + "step2");
                    cyclicBarrier.await();

                    System.out.println(Thread.currentThread() + "step3");
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        });

        // 关闭线程池
        executorService.shutdown();
    }
}

    以上代码中,每个子线程在执行完 step1 后都调用了 await 方法,等到所有线程都到达屏障点后,才会一起执行,这就保证了 所有线程都完成了阶段 1 后 才会开始执行阶段2 … … 以此类推。

运行结果:

java wms第三方同步池实现方式_CyclicBarrier_04

2、类图

java wms第三方同步池实现方式_CountDownLatch_05


    可以看到,CyclicBarrier 基于独占锁实现,本质底层还是基于 AQS 的。使用 lock 保证更新 计数器 count 的原子性 (另外使用 lock 的条件变量 trip 之处线程间使用 awiat 和 signal 操作进行同步)。parties 用于记录线程个数,表示多少线程调用 await 方法之后,所有线程才会冲破屏障,继续往下运行。而 count 一开始等于 parties,每当有线程调用 await 方法,count 就递减 1 ,当 count 为 0 时,就表示所有线程都到了屏障点。❓为s什么要维护 parties 和 count 两个变量呢 ❓ 因为 CycleBarrier 是可以被复用的,parties 始终记录总的线程个数,当 count 计数器值 变为 0 后,会将 parties 的值 赋给 count 从而进行复用。这两个变量在构造 CyclicBarrier 对象时传递的:

public CyclicBarrier(int parties, Runnable barrierAction) {
        if (parties <= 0) throw new IllegalArgumentException();
        this.parties = parties;
        this.count = parties;
        this.barrierCommand = barrierAction;
    }

    barrierCommand 是任务,当所有线程都到达屏障点后,就会执行这个任务。
     在 变量 generation 内部有一个变量 broken用来记录当前屏障 是否被打破,它没有被声明成 volatile ,因为是在锁内使用的,可以保证内存可见性:

private static class Generation {
        boolean broken = false;
    }

    

3、实现原理探究
(1) int await() 方法

    当前线程调用该方法时会被阻塞,直到满足以下条件之一 才会返回:

  • parties 个线程都调用了 await() 方法,也就是说 所有的线程都到了屏障点。
  • 其他线程调用了该线程的 interrupt() 方法 中断了当前线程,则当前线程会抛出 InterruptedException 异常而返回。
  • 与 当前屏障点 关联的 Generation 对象 的 broken 标志被设置为 true,会抛出 BrokenBarrierException 异常,然后返回。

源码:

public int await() throws InterruptedException, BrokenBarrierException {
        try {
            return dowait(false, 0L);
        } catch (TimeoutException toe) {
            throw new Error(toe); // cannot happen
        }
    }

    可以看到,内部调用了 dowait() 方法,第一个参数是 false,表示 不设置超时时间,第二个参数没有意义。
    

(2) await(long timeout, TimeUnit unit) 方法

    当前线程调用该方法时会被阻塞,直到满足以下条件之一 才会返回:

  • parties 个线程都调用了 await() 方法,也就是说 所有的线程都到了屏障点。
  • 其他线程调用了该线程的 interrupt() 方法 中断了当前线程,则当前线程会抛出 InterruptedException 异常而返回。
  • 与 当前屏障点 关联的 Generation 对象 的 broken 标志被设置为 true,会抛出 BrokenBarrierException 异常,然后返回。
  • 设置的超时时间到了后 返回 false。

源码:

public int await(long timeout, TimeUnit unit)
        throws InterruptedException,
               BrokenBarrierException,
               TimeoutException {
        return dowait(true, unit.toNanos(timeout));
    }

    可以看到,内部调用了 dowait() 方法,第一个参数是 true,表示 设置了超时时间,第二个参数是超时时间。
    

(3) int dowait(boolean timed, long nanos) 方法

    该方法是 CyclicBarrier 的核心。

源码:

private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException,
               TimeoutException {
        final ReentrantLock lock = this.lock;
        
        // 获取独占锁
        lock.lock();
        try {
            final Generation g = generation;

            if (g.broken)
                throw new BrokenBarrierException();

            if (Thread.interrupted()) {
                breakBarrier();
                throw new InterruptedException();
            }

            int index = --count;
            
             // 如果 index == 0,说明所有线程都到了屏障点
            if (index == 0) {  // tripped
             
                boolean ranAction = false;
                try {
                    final Runnable command = barrierCommand;
                    
                    if (command != null)
                    	// 执行任务
                        command.run();
                    ranAction = true;
                    
                    // (一)激活其他因为调用 await 方法而被阻塞的线程,并重置 CyclicBarrier
                    nextGeneration();
                    return 0;
                } finally {
                    if (!ranAction)
                        breakBarrier();
                }
            }

            // loop until tripped, broken, interrupted, or timed out
            for (;;) {
                try {
                    if (!timed)
                        trip.await();
                        
                    else if (nanos > 0L)
                        nanos = trip.awaitNanos(nanos);
                } catch (InterruptedException ie) {
                    if (g == generation && ! g.broken) {
                        breakBarrier();
                        throw ie;
                    } else {
                        // We're about to finish waiting even if we had not
                        // been interrupted, so this interrupt is deemed to
                        // "belong" to subsequent execution.
                        Thread.currentThread().interrupt();
                    }
                }

                if (g.broken)
                    throw new BrokenBarrierException();

                if (g != generation)
                    return index;

                if (timed && nanos <= 0L) {
                    breakBarrier();
                    throw new TimeoutException();
                }
            }
        } finally {
            lock.unlock();
        }
    }

(一)nextGeneration():

private void nextGeneration() {
        // signal completion of last generation
        trip.signalAll();
        // set up next generation
        count = parties;
        generation = new Generation();
    }

    当一个线程调用了 dowait 方法后,首先会获取独占锁 lock,比如,创建 CyclicBarrier 时传递的参数是 10,那么后面 9 个调用线程会被阻塞。然后 当前获取到锁的线程 会对 计数器 count 进行递减操作,递减后 count = index = 9,因为 index != 0,所以 当前线程会走到 for 循环。如果当前线程调用的是无参数的 await 方法,则 timed = false,所以 当前线程会被放入 条件变量的 trip 的条件阻塞队列,当前线程会被挂起并释放获取的 lock 锁。如果调用的是 有参数的 await 方法 则 timed = true,然后 当前线程也会被放入条件变量的条件队列中 并释放锁,不同的是 当前线程会在指定时间超时后 自动被激活。     当第一个获取锁的线程由于被阻塞 释放锁后,被阻塞的 9 个线程中 有一个会释放 lock 锁,然后执行与第一个线程同样的操作,直到最后一个线程获取到 lock 锁,此时 已经有 9 个线程被放入了 条件变量 trip 的条件队列中。最后 count = index = 0,如果创建 CyclicBarrir 时传递了任务,则 在其他线程被唤醒前 先执行任务,任务执行完毕后,再激活其他因为调用 await 方法而被阻塞的 9 个线程,并 重置 CyclicBarrier ,然后 这 10 个线程就可以继续向下进行啦。
    
🎭 总结:
    CyclicBarrier 与 CountDownLatch 的不同之处在于,前者是可以复用的,而且 特别适合 分段任务有序执行的场景 ,它是通过独占锁 ReentrantLock 实现计数器原子性更新,并使用条件变量队列 来实现线程同步。
    

三、 信号量 Semaphore

    与上述两个同步器不同, Semaphore 内部的计数器是递增的,并且在一开始初始化时 可以指定一个初始值,但是并不需要知道 需要同步的线程个数,而是在需要同步的地方调用 acquire 方法时 指定需要同步的线程个数。

1、使用

例 1 👀: 在主线程中开启两个子线程让他们执行,等所有子线程执行完毕后,主线程再继续向下执行:

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

public class SemaphoreTest1 {
    // 创建一个 Semaphore 实例
    private static Semaphore semaphore= new Semaphore(0);

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        // 将线程 A 添加到线程池
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                try{
                    System.out.println(Thread.currentThread() + " over");
                    semaphore.release();
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        });

        // 将线程 B 添加到线程池
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                try{
                    System.out.println(Thread.currentThread() + " over");
                    semaphore.release();
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        });

        // 等待子线程执行完毕,返回
        semaphore.acquire(2);
        System.out.println("all child thread over!");

        // 关闭线程池
        executorService.shutdown();
    }
}

    首先创建了一个 信号量实例,构造方法的入参是 0,说明信号量计数器的值是 0。然后 main 方法向线程池添加两个线程任务,在每个线程内部调用信号量的 release 方法,相当于让计数器值递增 1,最后在 main 线程例调用信号量的 acquire 方法,传参为 2 表示 调用 acquire 方法的线程会一直阻塞,直到信号量的计数 变为 2 才会返回。所以,如果构造 Semaphore 时 传递的参数是 N,并在 M 个线程中 调用了该信号量的 release 方法,那么 在 调用 acquire 使 M 个线程同步时 传递的参数应该是 M+N 。

运行结果:

java wms第三方同步池实现方式_Semaphore_06


例 2 👀:

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

public class SemaphoreTest2 {
    // 创建一个 Semapore 实例
    private static volatile Semaphore semaphore = new Semaphore(0);

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        
        // 将线程 A 添加到线程池
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                try{
                    System.out.println(Thread.currentThread() + "A task over");
                    semaphore.release();
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        });
        // 将线程 B 添加到线程池
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                try{
                    System.out.println(Thread.currentThread() + "A task over");
                    semaphore.release();
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        });
        
        // 等待子线程执行任务 A 完毕
        semaphore.acquire(2);

        // 将线程 C 添加到线程池
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                try{
                    System.out.println(Thread.currentThread() + "B task over");
                    semaphore.release();
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        });

        // 将线程 D 添加到线程池
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                try{
                    System.out.println(Thread.currentThread() + "B task over");
                    semaphore.release();
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        });
        
        // 等待子线程执行 B 完毕,返回
        semaphore.acquire(2);
        
        System.out.println("task is over");
        
        // 关闭线程池
        executorService.shutdown();
    }
}

    以上代码首先将线程 A 和 线程 B 加入线程池。主线程执行 semaphore.acquire(2); 后被阻塞,线程 A 和 线程 B 调用 release 方法后 信号量变成了 2 ,这时 主线程的acquire 方法会返回,返回后 信号量值为 0。然后主线程添加线程 C 和 线程 D到线程池,之后执行 semaphore.acquire(2); 被阻塞,当线程 C 和 D 执行完 release 方法后,主线程才返回。从一定程度上可以看出,Semaphore 在某种程度上 实现了 CyclicBarrier 的复用功能。
    

2、类图

java wms第三方同步池实现方式_Java并发同步器_07


    可以看到, Semaphore 也是使用 AQS 实现的,Sync 有两个实现类,用来指定获取信号量时 是否采用公平策略。AQS 的 state 值表示 当前持有的信号量个数。

构造方法源码,默认采用非公平策略:

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

    

3、实现原理
(1) void acquire() 方法

    当前线程调用该方法的目的是 希望获取一个信号量资源。如果当前信号量个数大于 0 ,则 当前信号量的计数会 减 1,然后该方法直接返回。否则 如果当前信号量个数等于 0,则 当前线程会被放入 AQS 阻塞队列。当其他线程调用了当前线程的 interrupt() 方法中断了当前线程时,当前线程会抛出 InterruptedException 异常。

public void acquire() throws InterruptedException {
   		// (一)传入的参数 为 1 ,说明要获取一个信号量资源
        sync.acquireSharedInterruptibly(1);
    }

(一)acquireSharedInterruptibly:

public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();

		// 调用 Sync 子类方法尝试获取,
        if (tryAcquireShared(arg) < 0)
        	// 获取失败则放入阻塞队列,然后再次尝试,如果失败 则调用 park 方法挂起当前线程
            doAcquireSharedInterruptibly(arg);
    }

     acquireSharedInterruptibly 里 调用了 Sync 子类的 tryAcquireShared 方法,这里的子类分为 非公平策略 NonfairSync 和 公平策略 FairSync。

  • NonfairSync 的 tryAcquireShared 方法:
protected int tryAcquireShared(int acquires) {
				  // (二)
            return nonfairTryAcquireShared(acquires);
        }

(二)nonfairTryAcquireShared:

final int nonfairTryAcquireShared(int acquires) {
            for (;;) {
            	// 获取当前信号量值
                int available = getState();

				// 计算当前剩余值
                int remaining = available - acquires;
	
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }

     可以看到,nonfairTryAcquireShared 方法 先获取当前信号量值,然后减去需要获取的值,得到 剩余的信号量个数。如果剩余值小于 0 说明 当前信号量个数满足不了需求,那么直接返回负数; 这时当前线程会被放入 AQS 阻塞队列。 如果剩余值大于 0 ,则使用 CAS 操作设置当前信号量为剩余值,然后返回剩余值。
     由于 NonfairSync 是非公平获取的,也就是说 先调用 aquire 方法获取信号量的线程 不一定比 后来者 先获取到信号量。比如:线程 A 先调用了 acquire 方法获取信号量,但是 当前信号量个数为 0,那么 线程 A 会被放入 AQS 的阻塞队列。过一段时间后 线程 C 调用了 release 方法释放了一个信号量,如果当前没有其他线程获取信号量,那么 线程A 就会被激活,然后获取该信号量,但是,如果线程 C 释放信号量后,线程 C 调用了 acquire 方法,那么线程 C 和 线程 A 都会去竞争能够信号量资源。如果采用非公平策略,由 nonfairTryAcquireShared 可知,线程 C 完全可以在线程 A 被激活前 或者 激活后 先于线程 A 获取到该信号量,也就是说,在这种模式下,阻塞线程 和 当前请求的线程是竞争关系,而不遵循 先来先得的策略。
    

  • FairSync 的 tryAcquireShared 方法:
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 阻塞队列,否则 就去获取。
    

(2) void acquire(int permits) 方法

     acquire() 方法只需要获取一个信号量值,而 acquire(int permits) 方法需要获取 permits 个。

源码:

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

    

(3)acquireUninterruptibly() 方法

     该方法与 acquire() 方法类似,不同在于 它对中断不响应

public void acquireUninterruptibly() {
        sync.acquireShared(1);
    }

    

(4) acquireUninterruptibly(int permits) 方法

     该方法与 acquire(int permits) 方法类似,不同在于 它对中断不响应

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

    

(5) void release() 方法

     该方法的作用是 把当前 Semaphore 对象的 信号量值 增加 1 ,如果当前有线程因为调用 acquire 方法被阻塞而被放入了 AQS 的阻塞队列,则 会根据公平策略 选择一个信号量个数能被满足的线程进行激活,激活的线程会尝试获取刚增加的信号量。

源码;

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

(一)releaseShared :

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

    可以看到,该方法先尝试释放资源,如果释放成功,则 调用 doReleaseShared(); ,会使用 unpark 方法 唤醒 AQS 队列中 因为调用 acquire 方法而被阻塞的 线程中 最先挂起的线程,即 头节点。
(二)tryReleaseShared:

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

    可以看到,release 方法中每次只会对信号量值增加 1,而 (二)tryReleaseShared 中是无限循环,使用 CAS 保证了 release 方法对信号量递增 1 的原子性操作。
    

(6)release(int permits)

    该方法与 不带参数 的 release 方法的不同之处在于,前者每次调用 会在信号量值 原来的基础上增加 permits ,而 不带参数的方法,每次增加 1。

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

    可以看到,调用的是 releaseShared ,说明 信号量是线程共享的 ,信号量没有和固定线程绑定,多个线程可以同时使用 CAS 去更新信号量的值 而不会被阻塞。
    
🎭 总结:
    Semaphore 完全可以达到 CountDownLatch 的效果 ,但是 Semaphore 的计数器是不可以自动重置的,不过可以变相改变 acquire 方法的参数,来重置。Semaphore 也是使用 AQS 实现的,并且 获取信号量时 有 公平策略 和 非公平策略。
    

四、总结

    首先 ,CountDownLatch 通过计数器提供了更灵活的控制,只要检测到 计数器的值 为 0,就可以往下执行,这比使用 join 必须等待线程执行完毕后 主线程才能继续向下运行 灵活。
    另外,CyclicBarrier 也可以达到 CountDownLatch 的效果,但是 CountDownLatch 在计数器值变为 0 后,就不能再被复用,而 CyclicBarrier 可以提供 reset 方法重置后使用。而且 CyclicBarrier 对于 同一个算法 但是输入参数不同的 类似情景 比较适用。
    而 Semaphore 采用了信号量递增的策略,一开始并不需要关心同步的线程个数,等调用 acquire 方法时 再指定需要同步的个数,并且提供了获取信号量的 公平 与 非公平策略。
    使用同步器吧😁 ,再日常开发中大大减少 wait、notify 来实现线程同步,使用同步器可以节省很多代码,而且保证正确性。