Java中的四种同步工具类(感谢你,苏小落)

117 阅读5分钟




​前言苏宁估计是今年最大的黑马了,我们在开始中没有期望能战胜G2小组第一出线,在八进四中我们也没有没想到苏宁能够打赢京东,结果苏宁3:1战胜京东,我们认为苏宁今年的世界赛的成绩已经很好了,或许也就止步于此了,毕竟他作为LPL三号种子,接下来要面对的是一号种子Tes,而且Tes刚经历了一场让二追三的血腥厮杀,状态火热。苏宁用实力告诉你,不,我要追求更高的目标,他们战胜了Tes,成为了LPL 唯一的希望。\

现在我们唯一的期待,10月31号,我们能听到一句恭喜苏宁,恭喜LPL!

这波,感谢一波大校,没问题吧!周六大校支持一波大乌龟没问题吧
感谢你,苏小落回归主题,今天给大家分享个简单的知识点,JDK并发包里面自带的一些工具类,功能很强大实用

CountDownLatch

CountDownLatch就像一个门,门上有N把锁,只有当锁同时都打开,我们才能开门。这里这个门是一次性的,用完之后不能重新再给这个门上锁,为什么我要强调一次性呢?因为下面还有不是一次性的CyclicBarrier

CountDownLatch,使一个线程等待其他线程都达到相应的状态再执行,完成工作的主要方法就是await()、countDown()

内部是通过一个计数器实现的,计数器的初始值就是要等待线程的数量,每当一个线程执行完毕调用countDown()之后计数器数值减一,计数器数值变为0的时候,表示所有线程执行完毕,调用await()方法等待的线程便会恢复工作

来看一个例子:

public class TestCountDownLatch {

    public static void main(String[] args) {
        // 传入参数2,代表需要等待两个线程
        final CountDownLatch latch = new CountDownLatch(2);
        //线程等待一秒
        new Thread(new Runnable() {

            public void run() {
                try {
                    Thread.sleep(1000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("一号准备完毕,用时1秒!");
                latch.countDown();
            }
        }).start();
        //线程等待三秒
        new Thread(new Runnable() {
            public void run() {
                try {
                    Thread.sleep(2000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("二号准备完毕,用时2秒!");
                latch.countDown();
            }
        }).start();
        try {
            System.out.println("两位选手请准备!");
            //主线程调用await,等待两个线程到达(即调用countDown)
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 两个线程执行完毕后,主线程恢复运行
        System.out.println("准备完毕,开始!");
    }
}

看下运行结果:

两位选手请准备!
一号准备完毕,用时1秒!
二号准备完毕,用时2秒!
准备完毕,开始!

其实看到这里大家应该都懂了,没啥可说的,太简单了,接下来一起简单分析下源码,了解下它是如何实现的吧

CountDownLatch是基于AQS的同步器

/**
 * Constructs a {@code CountDownLatch} initialized with the given count.
 *
 * @param count the number of times {@link #countDown} must be invoked
 *        before threads can pass through {@link #await}
 * @throws IllegalArgumentException if {@code count} is negative
 */
public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}

看下CountDownLatch的构造函数,就是传入一个int数值,这个数值就是状态变量,点击Sync()发现是一个setState的设置状态的函数,再点进去发现进去的是AQS的一个方法,count就是AQS内部的一个volatile变量

/**
 * The synchronization state.
 */
private volatile int state;

volatile代表着啥我就不用多说了吧,还不清楚的传送门在这里学习volatile这一篇就够了,保证了可见性,有序性\

我们看下内部的await()和counDown()的实现方法:\

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

发现内部的实现都是通过sync实现的,这个sync对象时何方神圣呢?\

/**
 * Synchronization control For CountDownLatch.
 * Uses AQS state to represent count.
 */
private static final class Sync extends AbstractQueuedSynchronizer { }

哦,原来如此,是AQS的实现类啊原来,也就是CountDownLatch内部的主要方法是通过AQS来实现的\

前面的我是看懂了,但是这个AQS到底是个什么东西呢?AQS是一个多线程访问共享资源的同步器框架,资源共享的方式有两种,即独占Exclusive和共享Share;独占即只有一个线程能够执行,控制并发安全,例如ReentrantLock,共享即多个线程可以同时执行,比如我们现在说的CountDownLatch

内部通过维护了一个state共享资源和一个FIFO线程等待队列来实现的,自定义同步器时只需要实现共享资源state的获取和释放的方式即可。



CyclicBarrier

上面说到了一次性的锁,有时我们可能需要重复设置这个锁,这个场景如何满足呢?CyclicBarrier可以满足这个场景,这个工具类是可以重复使用的,通过reset来设置即可

和CountDownLatch不同的是,CyclicBarrier在指定数量的线程到达之前必须互相等待,也是因为在等待的线程被释放之后可以重复使用

在网上看到的一种说法叫做人满发车,很合适,车没有满的时候车上乘客需要等待,车到达目的地之后再返回出发点,重新等待发车

看个例子:

public class TestCyclicBarrier {
    public static void main(String[] args) {
        //初始化四个线程
        int threadNum = 4;
        CyclicBarrier barrier = new CyclicBarrier(threadNum, new MyThread());
        for (int i = 0; i < threadNum; i++) {
            new TestThread(barrier).start();
        }
    }

    static class TestThread extends Thread {
        private CyclicBarrier cyclicBarrier;
        public TestThread(CyclicBarrier cyclicBarrier) {
            this.cyclicBarrier = cyclicBarrier;
        }

        @Override
        public void run() {
            System.out.println("线程" + Thread.currentThread().getName() + "正在执行");
            try {
                Thread.sleep(3000);      //以睡眠来模拟操作
                System.out.println("线程" + Thread.currentThread().getName() + "执行完毕,等待其他线程执行完成");
                cyclicBarrier.await();
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("所有线程执行完成,继续处理其他任务...");
        }
    }

    static class MyThread extends Thread {
        @Override
        public void run() {
            System.err.println("我是特殊任务");
        }
    }
}

输出结果如下:\

线程Thread-3正在执行
线程Thread-2正在执行
线程Thread-4正在执行
线程Thread-1正在执行
线程Thread-1执行完毕,等待其他线程执行完成
线程Thread-2执行完毕,等待其他线程执行完成
线程Thread-3执行完毕,等待其他线程执行完成
线程Thread-4执行完毕,等待其他线程执行完成
所有线程执行完成,继续处理其他任务...
所有线程执行完成,继续处理其他任务...
所有线程执行完成,继续处理其他任务...
所有线程执行完成,继续处理其他任务...
我是特殊任务

CyclicBarrier内部是通过一个ReentrantLock的锁对象来控制的,基于Condition条件队列来对线程进行阻塞

/** The lock for guarding barrier entry */
private final ReentrantLock lock = new ReentrantLock();
/** Condition to wait on until tripped */
private final Condition trip = lock.newCondition();

内部也是一个计数器,每当线程到达屏障点的时候调用await()将自己阻塞,计数器减一,当计数器变为0的时候所有因为调用await()方法阻塞的线程都会被唤醒\

CyclicBarrier和CountDownLatch的不同点:\

  1、CountDownLatch是调用await的线程等待,而CyclicBarrier是大家一起等,互相等待;

  2、CountDownLatch是一次性的,而CyclicBarrier是可以重复使用的;

在一定程度上,CyclicBarrier和CountDownLatch是相似的,在一些场景下两者均可实现,比如当多个线程共同达到同一条件,一起继续执行这种,两者均可实现


Semaphore

我们在商场找停车位,车位一般是固定的,车位属于共享资源,更多的车子可能会对这些固定数量的车位进行“抢夺”,这种情景好像上面两个工具类都不太好解决,JDK中提供了信号量Semaphore这个工具类,用于在多线程环境下能够协调各个线程对共享资源的正确、合理使用

初始化的时候需要为这个许可集传入一个数值,这个数值代表同一时刻能够访问共享资源的线程数量,线程通过acquire()获得一个许可,然后对共享资源进行操作,如果许可集分配完了,线程进入等待状态,知道其他线程释放许可才有机会获得许可。

看一个例子:

public class TestSemaphore {

    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(3);//3个停车位
        for (int i = 0; i <6 ; i++) {//模拟6部汽车
            new Thread(()->{
                try {
                    semaphore.acquire();//占到车位
                    System.out.println(Thread.currentThread().getName()+" 抢占到车位");
                    TimeUnit.SECONDS.sleep(5);//模拟车停5秒
                    System.out.println(Thread.currentThread().getName()+" 停车3秒后离开车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    semaphore.release();//释放停车位
                }
            },String.valueOf(i)).start();
        }
    }
}

输出结果是:\

2 抢占到车位
3 抢占到车位
1 抢占到车位
2 停车3秒后离开车位
3 停车3秒后离开车位
1 停车3秒后离开车位
0 抢占到车位
4 抢占到车位
5 抢占到车位
4 停车3秒后离开车位
0 停车3秒后离开车位
5 停车3秒后离开车位

即初始化停车位数量(许可集),每个车子代表一个线程,进入停车场会获得一个许可,占用共享资源。一旦停车位全部被占,未分配到车位的车子进入等待状态,等其它车子释放车位才有机会获得车位

Semaphore内部也是通过AQS实现的,当许可集的数量设置成1的时候可以来做一个互斥锁,感兴趣的同学自己去研究发觉


Exchanger

还有一个常见的工具类就是Exchanger,这个我们听名字估计就能猜出大概,但凡英语过个四级这个单词应该都认识

Exchanger,就是提供了两个线程互相交换数据的同步点,即一个线程完成一定的事务之后想要和另一个线程交换数据,则需要拿出数据,等待另一个线程的到来。

public class TestExchanger {
    public static void main(String[] args) {
        Exchanger<String> exchanger = new Exchanger<>();
        new Thread(new Runnable() {
            @Override
            public void run() {
                String book = "《Java核心技术卷一》";
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("甲带着" + book + "到达交易地点!");
                try {
                    System.out.println("甲换出了:" + book + ",换回了:" + exchanger.exchange(book));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                String money = "200元";
                try {
                    Thread.sleep(4000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("乙带着" + money + "到达交易地点!");
                try {
                    System.out.println("乙换出了:" + money + ",换回了:" + exchanger.exchange(money));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

输出结果:

甲带着《Java核心技术卷一》到达交易地点!
乙带着200元到达交易地点!
甲换出了:《Java核心技术卷一》,换回了:200元
乙换出了:200元,换回了:《Java核心技术卷一》

Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。它提供一个同步点,在这个同步点两个线程可以交换彼此的数据。这两个线程通过exchange方法交换数据, 如果第一个线程先执行exchange方法,它会一直等待第二个线程也执行exchange,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。

因此使用Exchanger的重点是成对的线程使用exchange()方法,当有一对线程达到了同步点,就会进行交换数据。因此该工具类的线程对象是成对的

好了,以上就是全部内容了,我是,你们的学习成长小伙伴    

我希望有一天能够靠写字养活自己,现在还在磨练,这个时间可能会有很多年,感谢你们做我最初的读者和传播者。请大家相信,只要给我一份爱,我终究会还你们一页情的。

再次感谢大家能够读到这里,我后面会持续的更新技术文章以及一些记录生活的灵魂文章,如果觉得不错的,觉得有点东西的话,求点赞、关注、分享三连

哦,对了!后续的更新文章我都会及时放到这里,欢迎大家点击观看,都是干货文章啊,建议收藏,以后随时翻阅查看

github.com/DayuMM2021/…

推荐阅读

● 小白读了这篇JVM,直呼真香,淦!(长篇干货预警)

   ● 一篇搞懂ElasticSearch(附学习脑图)