投稿    登录
欢迎加入Nice Coder,与众多Coder分享经验,群号:530244901

【原创】鸡年第一更,同步工具类之闭锁,信号量,栅栏介绍。

java wjx@admin.cc 188浏览 0评论

鸡年到,站长王镜鑫祝各位鸡年大吉!

在写并发程序的时候,我们经常需要协调各个线程,以达到我们想要的效果。java.util.concurrent中给我们提供了一些工具类来实现这些需求。本篇文章将介绍以下几种同步工具类。

  1.    闭锁CountDownLatch
  2.    信号量Semaphore
  3.    栅栏CyclicBarrier

闭锁:

      闭锁是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。他的作用就像是一个锁,被这个锁锁住的线程都无法继续进行下去,直到这个锁被打开。并且这个锁被打开之后,就再也不会被锁上。闭锁可以保证某个活动在其他的某些活动都完成之后才继续执行。那么这个闭锁有什么实际的应用呢?有很多,比如我们再玩PVP游戏的时候,在左右的玩家都就绪之后才继续进行,这就需要用到闭锁,或者我们想要启动一个服务,前提是这个服务所依赖的一些资源必须都被加载完毕,这时候也需要用闭锁。

      CountDownLatch是一种灵活的闭锁实现,这个类有一个唯一的构造方法,这个构造方法需要传入一个int类型的变量,表示这个闭锁的计数器。这个计数器必须是个正数,这个类有两个很重要的方法,一个叫countDown另一个是await,这两个方法都不用穿入参数。第二个方法的意义是,调用该方法的线程会一直阻塞,直到计数器为0,或者线程被中断,或者等待超时,第一个方法的作用就是让计数器递减。下面将举一个实际应用的例子说明这个类。

  思考以下的需求:

      我们想要测试十个线程同时执行十次任务所需要的事件。我们可以在一开始先记录下来时间戳。然后启动十个线程,然后在每个线程结束之后输出结束的时间戳,找到最大的一个,也就是最后一个,这是我们能想到的最简便的一个方式,可是这种方式合适吗?显然是不合适的。这样的话,先启动的线程就会领先于后启动的线程,这时候,活跃的线程就会随着时间的推移会增加或者减少,这样线程之间的竞争度也会不断地被改变。这种条件下测试的结果不是我们想要的结果,所以我们需要求助于闭锁,使这十个线程同时开始。而且我们输出的策略是,每个线程在结束之后自己输出,这样的数据就更不可信了,因为输出所用的时间,可能会远远大于该线程中的任务执行的事件。所以我们需要求助于闭锁,使所有的线程都停止之后再去输出最后 的时间戳。所以从哪一个角度来看,当前的思路都不行,我们需要求助一下闭锁。(这里注意一下,“所有的线程同时开始”是不可能的。即使是使用闭锁,也会有先后顺序。)

  需求分析:

      我们需要这十个线程开始的时候执行操作A,结束的时候执行操作B。A和B操作都是输出时间戳,这两个操作可以放到主线程中去执行,也就是说我们在启动十个线程的时候,在开启之后,执行任务之前,需要阻塞该线程,直到这十个线程都被开启,这时候在同时启动,所以开启只需要执行一次,我们需要一个计数器为1的闭锁来控制同时开始。对于结束,我们是在这十个线程都结束之后才会输出时间戳,我们可以在主线程中一直阻塞,直到十个线程都执行完毕,也就是说这十个线程控制着主线程什么时候被唤醒,所以我们需要一个计数器为10的闭锁来控制结束。代码如下:

package baseModel;

import java.util.concurrent.CountDownLatch;

/**
 * Created by 王镜鑫 on 2017/1/28 15:58.
 */
public class TimeTest
{
    public static void main(String[] args) throws InterruptedException
    {
        int count = 10;//线程数
        CountDownLatch start = new CountDownLatch(1);//控制开始的闭锁。
        CountDownLatch end = new CountDownLatch(count);//控制结束的闭锁
        for(int i=0;i<count;i++)//启动十个线程
        {
            new Thread(()->{
            try
                {
                    start.await();//启动之后立马使用启动闭锁阻塞
                } catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
                System.out.println(Math.sin(Math.PI/2));//执行工作,计算一个正弦值
                end.countDown();//结束闭锁的计数器减一
            }).start();
        }
        System.out.println(System.nanoTime());//上面十个线程都在阻塞的时候立马输出当前时间戳
        start.countDown();//开始闭锁计数器减一,这时候所有线程被唤醒
        end.await();//立马使用结束闭锁阻塞。直到十个线程全部结束,也就是计数器减为0
        System.out.println(System.nanoTime());//输出时间戳。
    }
}

这段代码的执行结果如下:

195438299684476
1.0
1.0
1.0
1.0
1.0
1.0
1.0
1.0
1.0
1.0
195438314686597

以上就是闭锁的用法。

信号量:

      信号量常用来控制同时访问某个资源的操作的数量。比如去饭店吃饭,正好赶上饭点,人比较多,饭店的座位有限,只能一次进20个人,结果来了20个人,那么先来的那10个就会先进去吃,剩下的十个人就会一直被阻塞,直到先进去的那十个人有人出来。剩下的十个才能进去。信号量可以用来实现线程池。Semaphore这个类就是一种计数信号量的实现,这个类的构造方法中可以传进去一个正整数,用来表示许可的数量。他也有两个重要的方法,分别是acquire和release,这两个方法分别是获取一个许可,和释放一个许可,如果现在没有可用的许可,那么acquire方法会一直阻塞当前线程,直到出现一个可用的许可为止。信号量常用于实现某种资源池,比如线程池。在许可只有一个的时候,信号量就相当于是一个互斥锁,谁拿到这个许可,谁就能执行。这个就不在演示代码。

栅栏:

      栅栏和闭锁比较类似。栅栏的作用是等待一组线程,当这组线程都达到栅栏为止的时候,执行某个操作。闭锁和栅栏的区别就是,闭锁是一个线程等待 其他的线程完成一个事件,而栅栏是一组线程在互相等待,大家都要完成。闭锁是一次性的,栅栏可以重复使用。CyclicBarrier是一种栅栏的实现,他有两个构造方法,这两个构造方法都要求传入一个正整数,表示这个栅栏的参与者的数目,不同的是,其中一个构造方法还要求传入一个实现了Runnable接口的预定义的操作。在所有的参与者都到达栅栏位置的时候,执行该预定义操作。这个类有一个重要方法,叫await,这个方法很简单,执行到了这个方法就代表到达了栅栏位置。到达这个位置的线程将被阻塞,直到所有的线程都到达了这个位置,然后所有的线程继续执行,如果有第二个栅栏,在所有的线程到达第二个时候又会开始阻塞,直到所有的线程都到达这个栅栏。

  具体应用:

      十个人跑步,他们约定一起跑,然后在跑到中间的时候停下来。等所有人到齐了,喝口水,再接着跑,然后跑到终点等人都到到齐了就在喝口水。这时候就可以用栅栏做了。下面是参考代码。

package baseModel;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

/**
 * Created by 王镜鑫 on 2017/1/28 13:02.
 */
class Worker implements Runnable//跑步的人。
{
    private int id;//跑步的人有个id
    private CyclicBarrier barrier;//给跑步的人设置的栅栏
    public Worker(int id,final CyclicBarrier barrier)//构造方法,传入id和栅栏
    {
        this.id = id;
        this.barrier = barrier;
    }
    public void run()
    {
        try
        {
            System.out.println(this.id+" is ready!");//这个人准备好了
            Thread.sleep((long) (Math.random() * 10000));//开跑,跑步的快慢不同,所以跑步的事件不同
            System.out.println(this.id + " has reached position A");//这个人到达了A点
            barrier.await();//遇到栅栏,等待其他的人
            Thread.sleep((long) (Math.random() * 10000));//所有的人都到了,喝口水再接着跑,跑的时间也不一样。
            System.out.println(this.id + " has reached position B");//这个人到了B点
            barrier.await();//遇到栅栏,等待其他的人
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }
    }
}
public class BarrierTest
{
    public static void main(String[] args)
    {
        int num = 10;//人数
        CyclicBarrier barrier = new CyclicBarrier(num,()->System.out.println("Let's go together!"));//构造一个栅栏,参与者10人,定义参与者都到达栅栏之后的操作
        for(int i=1;i<=num;i++) new Thread(new Worker(i,barrier)).start();//是个参与者启动。传入同样的栅栏
    }
}

上述代码的运行结果如下:

1 is ready!
2 is ready!
3 is ready!
4 is ready!
5 is ready!
6 is ready!
7 is ready!
8 is ready!
9 is ready!
10 is ready!
4 has reached position A
6 has reached position A
2 has reached position A
7 has reached position A
10 has reached position A
9 has reached position A
8 has reached position A
3 has reached position A
5 has reached position A
1 has reached position A
Let's go together!
3 has reached position B
5 has reached position B
1 has reached position B
9 has reached position B
7 has reached position B
8 has reached position B
10 has reached position B
2 has reached position B
6 has reached position B
4 has reached position B
Let's go together!

上面的代码符合我们开始的预测。

转载请注明:王镜鑫的个人博客 » 【原创】鸡年第一更,同步工具类之闭锁,信号量,栅栏介绍。

喜欢 (6)or分享 (0)

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请狠狠点击下面的