闭锁,栅栏,信号量

闭锁CountDownLatch

Latch闭锁的意思,是一种同步的工具类。类似于一扇门:在闭锁到达结束状态之前,这扇门一直是关闭着的,不允许任何线程通过,当到达结束状态时,这扇门会打开并允许所有的线程通过。且当门打开了,就永远保持打开状态。门的状态也就不能变了,只能是打开状态。也就是说闭锁的状态是一次性的,它确保在闭锁打开之前所有特定的活动都需要在闭锁打开之后才能完成。

作用:可以用来确保某些活动直到其他活动都完成后才继续执行。

使用场景:

某个操作需要的资源初始化完毕

某个服务依赖的线程全部开启等等...

CountDowmLatch是一种灵活的闭锁实现,包含一个计数器,该计算器初始化为一个正数,表示需要等待事件的数量。countDown方法递减计数器,表示有一个事件发生,而await方法等待计数器到达0,表示所有需要等待的事情都已经完成

还有await(long timeout, TimeUnit unit),表示等待的时间

getCount()返回当前计数器的值,如果闭锁打开,那么返回的肯定就是0.

举一个例子,三个工人在为老板干活,这个老板有一个习惯,就是当三个工人把一天的活都干完了的时候,他就来检查所有工人所干的活。记住这个条件:三个工人先全部干完活,老板才检查

public class Work implements Runnable{


        private CountDownLatch downLatch;
        private String name;

        public Work(CountDownLatch downLatch, String name){
            this.downLatch = downLatch;
            this.name = name;
        }

        public void run() {
            this.doWork();
            try{
                TimeUnit.SECONDS.sleep(new Random().nextInt(10));
            }catch(InterruptedException ie){
            }
            System.out.println(this.name + "活干完了!");
            this.downLatch.countDown();

        }

        private void doWork(){
            System.out.println(this.name + "正在干活!");
        }

    }
public class Boss implements Runnable{

        private CountDownLatch downLatch;

        public Boss(CountDownLatch downLatch){
            this.downLatch = downLatch;
        }

        public void run() {
            System.out.println("老板正在等所有的工人干完活......");
            try {
                this.downLatch.await();
            } catch (InterruptedException e) {
            }
            System.out.println("工人活都干完了,老板开始检查了!");
        }

    }
public class TestLatch {

    public static void main(String[] args) {
            ExecutorService executor = Executors.newCachedThreadPool();

            CountDownLatch latch = new CountDownLatch(3);

            Work w1 = new Work(latch,"张三");
            Work w2 = new Work(latch,"李四");
            Work w3 = new Work(latch,"王二");

            Boss boss = new Boss(latch);

            executor.execute(w3);
            executor.execute(w2);
            executor.execute(w1);
            executor.execute(boss);

            executor.shutdown();
        }

    }

李四正在干活!
老板正在等所有的工人干完活......
王二正在干活!
张三正在干活!
李四活干完了!
王二活干完了!
张三活干完了!
工人活都干完了,老板开始检查了!
 


栅栏CyclicBarrier

允许一组线程在到达某个栅栏点(common barrier point)互相等待,发生阻塞,直到最后一个线程到达栅栏点,栅栏才会打开,处于阻塞状态的线程恢复继续执行.它非常适用于一组线程之间必需经常互相等待的情况。CyclicBarrier字面理解是循环的栅栏,之所以称之为循环的是因为在等待线程释放后,该栅栏还可以复用。

模拟一下对战平台中玩家需要完全准备好了,才能进入游戏的场景。

public class BarrierDemo {
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(5);
        final CyclicBarrier barrier = new CyclicBarrier(5);
        for (int i = 0; i < 5; i++) {
            service.execute(new Player("玩家" + i, barrier));
        }
        service.shutdown();
    }
}

public class Player implements Runnable {
    private final String name;
    private final CyclicBarrier barrier;

    public Player(String name, CyclicBarrier barrier) {
        this.name = name;
        this.barrier = barrier;
    }

    public void run() {
        try {
            TimeUnit.SECONDS.sleep(1 + (new Random().nextInt(3)));
            System.out.println(name + "已准备,等待其他玩家准备...");
            barrier.await();
            TimeUnit.SECONDS.sleep(1 + (new Random().nextInt(3)));
            System.out.println(name + "已加入游戏");
        } catch (InterruptedException e) {
            System.out.println(name + "离开游戏");
        } catch (BrokenBarrierException e) {
            System.out.println(name + "离开游戏");
        }

    }
}

玩家0已准备,等待其他玩家准备...
玩家2已准备,等待其他玩家准备...
玩家1已准备,等待其他玩家准备...
玩家4已准备,等待其他玩家准备...
玩家3已准备,等待其他玩家准备...
玩家4已加入游戏
玩家1已加入游戏
玩家0已加入游戏
玩家3已加入游戏
玩家2已加入游戏

CyclicBarrier有两个构造函数:

public CyclicBarrier(int parties)
public CyclicBarrier(int parties, Runnable barrierAction)

参数parties指定线程数量,当指定的线程值都到达栅栏点时,栅栏打开,线程恢复。需要注意的是,当指定的线程数量大于启动的线程数量,比如修改上例中的代码,只启动9个线程,那么所有的线程将一直处于等待状态。第二种情况是指定的线程数量小于启动的线程,上例代码,启动11个线程,那么当第十个线程到达栅栏点时,那么这十个线程就会恢复继续执行,而第十一个线程将一直处于阻塞状态

下面这个例子使用了第二个构造方法,同时验证了栅栏的可重用性。

public class CyclicBarrierTest {
    private final CyclicBarrier barrier;
    private final Worker[] workers;

    public CyclicBarrierTest(){
        int count = Runtime.getRuntime().availableProcessors();
        this.barrier = new CyclicBarrier(count,
                new Runnable(){

                    @Override
                    public void run() {
                        System.out.println("所有线程均到达栅栏位置,开始下一轮计算");
                    }

        });
        this.workers = new Worker[count];
        for(int i = 0; i< count;i++){
            workers[i] = new Worker(i);
        }
    }
    private class Worker implements Runnable{
        int i;

        public Worker(int i){
            this.i = i;
        }

        @Override
        public void run() {
            for(int index = 1; index < 3;index++){
                System.out.println("线程" + i + "第" + index + "次到达栅栏位置,等待其他线程到达");
                try {
                    //注意是await,而不是wait
                    barrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    return;
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                    return;
                }
            }
        }

    }

    public void start(){
        for(int i=0;i<workers.length;i++){
            new Thread(workers[i]).start();
        }
    }

    public static void main(String[] args){
        new CyclicBarrierTest().start();
    }
}

执行结果为:  
线程0第1次到达栅栏位置,等待其他线程到达  
线程1第1次到达栅栏位置,等待其他线程到达  
线程2第1次到达栅栏位置,等待其他线程到达  
线程3第1次到达栅栏位置,等待其他线程到达  
所有线程均到达栅栏位置,开始下一轮计算  
线程3第2次到达栅栏位置,等待其他线程到达  
线程2第2次到达栅栏位置,等待其他线程到达  
线程0第2次到达栅栏位置,等待其他线程到达  
线程1第2次到达栅栏位置,等待其他线程到达  
所有线程均到达栅栏位置,开始下一轮计算 
await()

Waits until all parties have invoked await on this barrier.

调用该方法会使当前线程在栅栏点发生阻塞,直到指定的线程数量都达到栅栏点时恢复执行

await(long timeout, TimeUnit unit)

Waits until all parties have invoked await on this barrier, or the specified waiting time elapses.

类似于await(),增加了超时时间参数。
当barrier在等待点出等待超时时,会抛出TimeoutException异常,同时,位于该barrier上的其他线程也将毁抛出BrokenBarrierException异常。这里说明,barrier上的线程要么同时成功要么同时失败,不存在部分成功部分失败的场景。

getNumberWaiting()

Returns the number of parties currently waiting at the barrier.

返回当前在栅栏处等待的参与者数目。此方法主要用于调试和断言。

getParties()

Returns the number of parties required to trip this barrier.

该方法可以获得构造函数中指定的需要在栅栏点阻塞的线程数量

isBroken()

Queries if this barrier is in a broken state

查询此栅栏是否处于损坏状态。

reset()

Resets the barrier to its initial state.

将barrier重置为其初始状态。如果所有参与者目前都在屏障处等待,则它们将返回,同时抛出一个 BrokenBarrierException


栅栏与闭锁的关键区别:

1.闭锁是一次性对象,栅栏可以重用,也可以reset()重置。

2.所有线程必须都到达栅栏位置,才能继续执行。闭锁用于等

待事件,而栅栏用于等待其他线程。


信号量Semaphore

Semaphore来实现信号量,概念上讲,一个信号量相当于持有一些许可(permits),线程可以调用Semaphore对象的acquire()方法获取一个许可,调用release()来归还一个许可

构造方法:
Semaphore有两个构造方法 Semaphore(int)Semaphore(int,boolean),参数中的int表示该信号量拥有的许可数量,boolean表示获取许可的时候是否是公平的,如果是公平的那么,当有多个线程要获取许可时,会按照线程来的先后顺序分配许可,否则,线程获得许可的顺序是不定的。这里在jdk中讲到 “一般而言,非公平时候的吞吐量要高于公平锁”,这是为什么呢?附上链接中的一段话:

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

获取许可
可以使用acquire()、acquire(int)、tryAcquire()等去获取许可,其中int参数表示一次性要获取几个许可,默认为1个,acquire方法在没有许可的情况下,要获取许可的线程会阻塞,而tryAcquire()方法在没有许可的情况下会立即返回 false,要获取许可的线程不会阻塞,这与Lock类的lock()与tryLock()类似

释放许可

线程可调用 release()、release(int)来释放(归还)许可,注意一个线程调用release()之前并不要求一定要调用了acquire (There is no requirement that a thread that releases a permit must have acquired that permit by calling {@link #acquire})

public static void main(String[] args) {
		ExecutorService pool =  Executors.newCachedThreadPool();
	        final Semaphore semaphore = new Semaphore(3,true);   
	        for (int i = 0; i < 10; i++) {
	            Runnable runnable = new Runnable() {
	                @Override
	                public void run() {
	                    try {
	                        semaphore.acquire();//获取信号灯许可
	                    } catch (InterruptedException e) {
	                        // TODO Auto-generated catch block
	                        e.printStackTrace();
	                    }
	                    System.out.println("Thread "+Thread.currentThread().getName()+
	                    		" 进入" +"当前系统的并发数是:"+(3-semaphore.availablePermits()));
	                    try {
	                        Thread.sleep(new Random().nextInt(1000));
	                    } catch (InterruptedException e) {
	                        // TODO Auto-generated catch block
	                        e.printStackTrace();
	                    }
	                    System.out.println("Thread "+Thread.currentThread().getName()+" 即将离开");
	                    semaphore.release();//释放信号灯
	                    System.out.println("Thread "+Thread.currentThread().getName()+
	                    		" 已经离开,当前系统的并发数是:"+(3-semaphore.availablePermits()));
	                }
	            };
	            pool.execute(runnable);
	        }
	 }

输出为:

Thread pool-1-thread-2 进入当前系统的并发数是:3

Thread pool-1-thread-1 进入当前系统的并发数是:3
Thread pool-1-thread-3 进入当前系统的并发数是:3
Thread pool-1-thread-2 即将离开
Thread pool-1-thread-2 已经离开,当前系统的并发数是:2
Thread pool-1-thread-5 进入当前系统的并发数是:3
Thread pool-1-thread-5 即将离开
Thread pool-1-thread-5 已经离开,当前系统的并发数是:2
Thread pool-1-thread-4 进入当前系统的并发数是:3
Thread pool-1-thread-3 即将离开
Thread pool-1-thread-3 已经离开,当前系统的并发数是:2
Thread pool-1-thread-6 进入当前系统的并发数是:3
Thread pool-1-thread-1 即将离开
Thread pool-1-thread-1 已经离开,当前系统的并发数是:2
Thread pool-1-thread-7 进入当前系统的并发数是:3
Thread pool-1-thread-6 即将离开
Thread pool-1-thread-6 已经离开,当前系统的并发数是:2
Thread pool-1-thread-9 进入当前系统的并发数是:3
Thread pool-1-thread-4 即将离开
Thread pool-1-thread-10 进入当前系统的并发数是:3
Thread pool-1-thread-4 已经离开,当前系统的并发数是:3
Thread pool-1-thread-7 即将离开
Thread pool-1-thread-7 已经离开,当前系统的并发数是:2
Thread pool-1-thread-8 进入当前系统的并发数是:3
Thread pool-1-thread-10 即将离开
Thread pool-1-thread-10 已经离开,当前系统的并发数是:2
Thread pool-1-thread-9 即将离开
Thread pool-1-thread-9 已经离开,当前系统的并发数是:1
Thread pool-1-thread-8 即将离开
Thread pool-1-thread-8 已经离开,当前系统的并发数是:0

当我们在构造Semaphore对象时,如果设置的许可数量为1,这时便会达到一个互斥排他锁的效果,只有一个许可,有一个线程获取了这个许可,那么其他线程只有等待这个线程归还了许可才能获取到许可,当将Semaphore用作互斥排他锁的作用时,要注意:

A semaphore initialized to one, and which is used such that it only has at most one permit available, can serve as a mutual exclusion lock. This is more commonly known as a binary semaphore, because it only has two states: one permit available, or zero permits available. When used in this way, the binary semaphore has the property (unlike many Lock implementations), that the “lock” can be released by a thread other than the owner (as semaphores have no notion of ownership). This can be useful in some specialized contexts, such as deadlock recovery.

文档中提到,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. 我们看到可用的许可数目增加了一个,但我们的初衷是保证只有一个许可来达到互斥排他锁的目的,所以这里要注意一下

2 Semaphore(1)可以做到一个deadlock recovery,我们来看下面一个例子

class WorkThread2 extends Thread{
    private Semaphore semaphore1,semaphore2;
    public WorkThread2(Semaphore semaphore1,Semaphore semaphore2){
        this.semaphore1=semaphore1;
        this.semaphore2=semaphore2;
    }
    public void releaseSemaphore2(){
        System.out.println(Thread.currentThread().getId()+" 释放Semaphore2");
        semaphore2.release();
    }
    public void run() {
        try {
            semaphore1.acquire(); //先获取Semaphore1
            System.out.println(Thread.currentThread().getId()+" 获得Semaphore1");
            TimeUnit.SECONDS.sleep(5); //等待5秒让WorkThread1先获得Semaphore2
            semaphore2.acquire();//获取Semaphore2
            System.out.println(Thread.currentThread().getId()+" 获得Semaphore2");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }   
    }
}
class WorkThread1 extends Thread{
        private Semaphore semaphore1,semaphore2;
        public WorkThread1(Semaphore semaphore1,Semaphore semaphore2){
            this.semaphore1=semaphore1;
            this.semaphore2=semaphore2;
        }
        public void run() {
            try {
                semaphore2.acquire();//先获取Semaphore2
                System.out.println(Thread.currentThread().getId()+" 获得Semaphore2");
                TimeUnit.SECONDS.sleep(5);//等待5秒,让WorkThread1先获得Semaphore1
                semaphore1.acquire();//获取Semaphore1
                System.out.println(Thread.currentThread().getId()+" 获得Semaphore1");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
}
public class SemphoreTest {
    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore1=new Semaphore(1);
        Semaphore semaphore2=new Semaphore(1);
        new WorkThread1(semaphore1, semaphore2).start();
        new WorkThread2(semaphore1, semaphore2).start();
        //此时已经陷入了死锁,WorkThread1持有semaphore1的许可,请求semaphore2的许可
        // WorkThread2持有semaphore2的许可,请求semaphore1的许可
//      TimeUnit.SECONDS.sleep(10);
//      //在主线程是否semaphore1,semaphore2,解决死锁
//      semaphore1.release();
//      semaphore2.release();
    }

}

在注释最后面几行代码的情况下,结果为,陷入了一个死锁:

9 获得Semaphore2
10 获得Semaphore1

把注释删除,即在主线程释放Semaphore,这样就能解决死锁:

9 获得Semaphore2
10 获得Semaphore1
9 获得Semaphore1
10 获得Semaphore2

这即符合文档中说的,通过一个非owner的线程来实现死锁恢复,但如果你使用的是Lock则做不到,可以把代码中的两个信号量换成两个锁对象试试。很明显,前面也验证过了,要使用Lock.unlock()来释放锁,首先你得拥有这个锁对象,因此非owner线程(事先没有拥有锁)是无法去释放别的线程的锁对象















猜你喜欢

转载自blog.csdn.net/u010365819/article/details/80803402