文章目录

  • 1. 独占锁
  • 2. 共享锁
  • 2.1 Semaphore
  • 2.2 CountDownLatch
  • 2.3 CyclicBarrie



问题:


1、Semaphore有什么作用?


2、CyclicBarrier和CountDownLatch的用法及区别


3、三个线程a、b、c并发运行,b,c需要a线程的数据怎么实现?


4、怎么控制同一时间只有 3 个线程运行?

1. 独占锁

独占锁也叫排他锁、互斥锁、独享锁,是指锁在同一时刻只能被一个线程所持有。一个线程加锁后,任何其他试图再次加锁的线程都会被阻塞,直到持有锁线程解锁。通俗来说,就是共享资源某一时刻只能有一个线程访问,其余线程阻塞等待。

如果是公平地独占锁,在持有锁线程解锁时,如果有一个以上的线程在阻塞等待,那么最先抢锁的线程被唤醒变为就绪状态去执行加锁操作,其他的线程仍然阻塞等待。

java中的Synchronized内置锁和ReentrantLock显式锁都是独占锁。

2. 共享锁

共享锁就是在同一时刻允许多个线程持有的锁。当然,获得共享锁的线程只能读取临界区的数据,不能修改临界区的数据。

JUC中的共享锁包括Semaphore(信号量)、ReadLock(读写锁)中的读锁、CountDownLatch倒数闩。

2.1 Semaphore

Semaphore用来限制能同时访问共享资源的线程上限。

Semaphore可以用来控制在同一时刻访问共享资源的线程数量,通过协调各个线程以保证共享资源的合理使用。Semaphore维护了一组虚拟许可,它的数量可以通过构造器的参数指定。线程在访问共享资源前必须调用Semaphore的acquire()方法获得许可,如果许可数量为0,该线程就一直阻塞。线程访问完资源后,必须调用Semaphore的release()方法释放许可。更形象的说法是:Semaphore是一个许可管理器。

Semaphore类的主要方法大致如下:

(1) Semaphore(permits):构造一个Semaphore实例,初始化其管理的许可数量为permits参数值。

(2) Semaphore(permits,fair):构造一个Semaphore实例,初始化其管理的许可数量为permits参数值,以及是否以公平模式(fair参数是否为true)进行许可的发放。

Semaphore和ReentrantLock类似,Semaphore发放许可时有两种模式:公平模式和非公平模式,默认情况下使用非公平模式。

(3) availablePermits():获取Semaphore对象可用的许可数量。

(4) acquire():当前线程尝试获取Semaphore对象的一个许可。此过程是阻塞的,线程会一直等待Semaphore发放一个许可,直到发生以下任意一件事:

  • 当前线程获取了一个可用的许可。
  • 当前线程被中断,就会抛出InterruptedException异常,并停止等待,继续往下执行。

(5) acquire(permits) :当前线程尝试阻塞地获取permits个许可。此过程是阻塞的,线程会一直等待Semaphore发放permits个许可。如果没有足够的许可而当前线程被中断,就会抛出InterruptedException异常并终止阻塞。

(6) acquireUninterruptibly():当前线程尝试阻塞地获取一个许可,阻塞的过程不可中断,直到成功获取一个许可。

(7) acquireUninterruptibly(permits):当前线程尝试阻塞地获取permits个许可,阻塞的过程不可中断,直到成功获取permits个许可。

(8) tryAcquire():当前线程尝试获取一个许可。此过程是非阻塞的,它只是进行一次尝试,会立即返回。如果当前线程成功获取了一个许可,就返回true;如果当前线程没有获得许可,就返回false

(9) tryAcquire(permits):当前线程尝试获取permits个许可。此过程是非阻塞的,它只是进行一次尝试,会立即返回。如果当前线程成功获取了permits个许可,就返回true;如果当前线程没有获得permits个许可,就返回false。

(10) tryAcquire(timeout,TimeUnit):限时获取一个许可。此过程是阻塞的,会一直等待许可,直到发生以下任意一件事:

  • 当前线程获取了一个许可,则会停止等待,继续执行,并返回true。
  • 当前线程等待timeout后超时,则会停止等待,继续执行,并返回false。
  • 当前线程在timeout时间内被中断,则会抛出InterruptedException异常,并停止等待,继续执行。

(11) tryAcquire(permits,timeout,TimeUnit):与tryAcquire(timeout,TimeUnit)方法在逻辑上基本相同,不同之处在于:在获取许可的数量上不同,此方法用于获取permits个许可。

(12) release():当前线程释放一个可用的许可。

(13) release(permits):当前线程释放permits个可用的许可。

(14) drainPermits():当前线程获得剩余的所有可用许可。

(15) hasQueuedThreads():判断当前Semaphore对象上是否存在正在等待许可的线程。

(16) getQueueLength():获取当前Semaphore对象上正在等待许可的线程数量。

共享锁使用示例 :

@Slf4j
public class TestSemaphore {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(3);
        for(int i=0;i<10;i++){
            new Thread(()->{
                try {
                    // 获取许可
                    semaphore.acquire();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 睡眠1s
                try {
                    log.debug("running.....");
                    Thread.sleep(1000);
                    log.debug("end.....");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    semaphore.release();
                }
            }).start();
        }
    }
}

执行结果:

17:36:28.044 [Thread-1] DEBUG com.example.aa.TestSemaphore - running.....
17:36:28.047 [Thread-0] DEBUG com.example.aa.TestSemaphore - running.....
17:36:28.047 [Thread-2] DEBUG com.example.aa.TestSemaphore - running.....
17:36:29.047 [Thread-0] DEBUG com.example.aa.TestSemaphore - end.....
17:36:29.047 [Thread-2] DEBUG com.example.aa.TestSemaphore - end.....
17:36:29.047 [Thread-3] DEBUG com.example.aa.TestSemaphore - running.....
17:36:29.047 [Thread-4] DEBUG com.example.aa.TestSemaphore - running.....
17:36:29.047 [Thread-1] DEBUG com.example.aa.TestSemaphore - end.....
17:36:29.047 [Thread-5] DEBUG com.example.aa.TestSemaphore - running.....
17:36:30.047 [Thread-4] DEBUG com.example.aa.TestSemaphore - end.....
17:36:30.047 [Thread-5] DEBUG com.example.aa.TestSemaphore - end.....
17:36:30.047 [Thread-6] DEBUG com.example.aa.TestSemaphore - running.....
17:36:30.047 [Thread-3] DEBUG com.example.aa.TestSemaphore - end.....
17:36:30.047 [Thread-7] DEBUG com.example.aa.TestSemaphore - running.....
17:36:30.047 [Thread-8] DEBUG com.example.aa.TestSemaphore - running.....
17:36:31.047 [Thread-7] DEBUG com.example.aa.TestSemaphore - end.....
17:36:31.047 [Thread-8] DEBUG com.example.aa.TestSemaphore - end.....
17:36:31.047 [Thread-6] DEBUG com.example.aa.TestSemaphore - end.....
17:36:31.047 [Thread-9] DEBUG com.example.aa.TestSemaphore - running.....
17:36:32.048 [Thread-9] DEBUG com.example.aa.TestSemaphore - end.....
2.2 CountDownLatch

用来进行线程同步协作,等待所有线程完成倒计时。 其中构造参数用来初始化等待计数值,await() 用来等待计数归零,countDown() 用来让计数减一。

CountDownLatch可以指定一个计数值,在并发环境下由线程进行减一操作,当计数值变为0之后,被await方法阻塞的线程将会唤醒。通过CountDownLatch可以实现线程间的计数同步。

@Slf4j
public class TestCountDownLatch {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(3);
        new Thread(()->{
            log.debug("begin...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 计数减1
            countDownLatch.countDown();
            log.debug("end...");
        }).start();

        new Thread(()->{
            log.debug("begin...");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 计数减1
            countDownLatch.countDown();
            log.debug("end...");
        }).start();

        new Thread(()->{
            log.debug("begin...");
            try {
                Thread.sleep(1500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 计数减1
            countDownLatch.countDown();
            log.debug("end...");
        }).start();
        
        log.debug("waiting...");
        // await() 用来等待计数归零
        countDownLatch.await();
        // 当计数值变为0之后,被await方法阻塞的线程将会唤醒
        log.debug("waiting end....");
    }
}

执行结果:

19:23:34.930 [pool-1-thread-2] DEBUG com.example.aa.TestCountDownLatch - begin...
19:23:34.930 [pool-1-thread-3] DEBUG com.example.aa.TestCountDownLatch - begin...
19:23:34.930 [pool-1-thread-1] DEBUG com.example.aa.TestCountDownLatch - begin...
19:23:34.932 [pool-1-thread-4] DEBUG com.example.aa.TestCountDownLatch - waiting...
19:23:35.933 [pool-1-thread-1] DEBUG com.example.aa.TestCountDownLatch - end...2
19:23:36.432 [pool-1-thread-3] DEBUG com.example.aa.TestCountDownLatch - end...1
19:23:36.933 [pool-1-thread-2] DEBUG com.example.aa.TestCountDownLatch - end...0
19:23:36.933 [pool-1-thread-4] DEBUG com.example.aa.TestCountDownLatch - waiting end....

可以配合线程池使用,改进如下:

@Slf4j
public class TestCountDownLatch {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(3);
        ExecutorService executorService = Executors.newFixedThreadPool(4);
        executorService.submit(()->{
            log.debug("begin...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 计数减1
            countDownLatch.countDown();
            log.debug("end...{}",countDownLatch.getCount());
        });

        executorService.submit(()->{
            log.debug("begin...");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 计数减1
            countDownLatch.countDown();
            log.debug("end...{}",countDownLatch.getCount());
        });

        executorService.submit(()->{
            log.debug("begin...");
            try {
                Thread.sleep(1500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 计数减1
            countDownLatch.countDown();
            log.debug("end...{}",countDownLatch.getCount());
        });

        executorService.submit(()->{
            log.debug("waiting...");
            // await() 用来等待计数归零
            try {
                countDownLatch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 当计数值变为0之后,被await方法阻塞的线程将会唤醒
            log.debug("waiting end....");
        });
    }
}

结合上述示例的运行结果,梳理一下CountDownLatch的使用步骤:

(1)创建倒数闩,初始化CountDownLatch时设置倒数的总次数,比如为3。

(2)等待线程调用倒数闩的await()方法阻塞自己,等待倒数闩的计数器数值为0(倒数线程全部执行结束)。

(3)倒数线程执行完,调用CountDownLatch.countDown()方法将计数器数值减一。

2.3 CyclicBarrie

用来进行线程协作,等待线程满足某个计数。构造时设置『计数个数』,每个线程执 行到某个需要“同步”的时刻调用 await() 方法进行等待,当等待的线程数满足『计数个数』时,继续执行

@Slf4j
public class TestCountDownLatch {
    public static void main(String[] args) throws InterruptedException {
        CyclicBarrier cb = new CyclicBarrier(2); // 个数为2时才会继续执行
        new Thread(()->{
            System.out.println("线程1开始.."+new Date());
            try {
                cb.await(); // 当个数不足时,等待
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            }
            System.out.println("线程1继续向下运行..."+new Date());
        }).start();
        new Thread(()->{
            System.out.println("线程2开始.."+new Date());
            try { Thread.sleep(2000); } catch (InterruptedException e) { }
            try {
                cb.await(); // 2 秒后,线程个数够2,继续运行
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            }
            System.out.println("线程2继续向下运行..."+new Date());
        }).start();
    }
}

执行结果:

线程1开始..Mon Mar 28 19:32:21 CST 2022
线程2开始..Mon Mar 28 19:32:21 CST 2022
线程2继续向下运行...Mon Mar 28 19:32:23 CST 2022
线程1继续向下运行...Mon Mar 28 19:32:23 CST 2022

CyclicBarrier 与 CountDownLatch 的主要区别在于 CyclicBarrier 是可以重用的,CyclicBarrier 可以被比 喻为『人满发车』

Semaphore、CountDownLatch二者都是基于共享锁实现的,用于在线程之间进行操作同步的工具类。JUC的同步工具类一共有3个:Semaphore、CountDownLatch和CyclicBarrier。