按照维基百科的解释:同步屏障(Barrier)是并行计算中的一种同步方法。对于一群进程或线程,程序中的一个同步屏障意味着任何线程/进程执行到此后必须等待,直到所有线程/进程都到达此点才可继续执行下文。

在ZK官网https://zookeeper.apache.org/doc/current/zookeeperTutorial.html ,提供了一个示例实现,但这个例子比较复杂,代码同时包括了Barrier和Queue两种实现,对例子做了修改,仅介绍Barrier的实现。

使用请客吃饭的场景:一张桌子坐四个人,四个人都到齐后,才能开饭;四个人都吃完以后,才能离开。

1      实现原理

为一个餐桌创建一个节点如/table-3,每一个客人是它的一个子节点/table-3/张三。所有客人都监听/table-3的事件,收到事件后检查子节点个数,如果达到要求的人数就开饭;当吃完以后,删除自己的子节点,并继续监听/table-3的事件,当子节点个数为0时,退出程序。

2      客人落座

落座的流程分两步:首先,创建自己的子节点;然后,等待其他客人落座直到坐满。创建客人子节点时CreateMode使用的是CreateMode.EPHEMERAL,这是属于当前zk会话的节点,当会话关闭时,如果节点没有删除,ZK会自动删除。

String nodeName = tableSerial + "/" + customerName;

log.info("{}: 自己坐下来 {}", customerName, nodeName);

zk.create(nodeName, new byte[0], Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);

while (true) {

synchronized (mutex) {

// 读出子节点列表,并继续监听

List<String> list = zk.getChildren(tableSerial, true);

if (list.size() < tableCapacity) {

log.info("{}: 当前人数 = {} , 总人数 = {}, 人还不够: 吃饭不积极,一定有问题...", customerName,

list.size(), tableCapacity);

mutex.wait();

} else {

log.info("{}: 人终于够了,开饭...", customerName);

return true;

}

}

}

3      客人准备离开

客人准备离开的逻辑同落座类似,首先删除自己的子节点,然后判断是否所有的子节点都已经被删除。删除子节点时,直接设置版本号为0,这是因为在这个示例中创建后没有修改过数据。真实业务场景,应该先读出zk中数据的版本号,然后作为参数传入到delete命令。

String nodeName = tableSerial + "/" + customerName;

log.info("{}: 已经吃完,准备离席,删除节点 {}", customerName, nodeName);

zk.delete(nodeName, 0);

while (true) {

// 读出子节点列表,并继续监听

List<String> list = zk.getChildren(tableSerial, true);

if (list.size() > 0) {

log.info("{}: 还有 {} 人没吃完,你们吃快点...", customerName, list.size());

synchronized (mutex) {

mutex.wait();

}

} else {

log.info("{}: 所有人都吃完了,准备散伙", customerName);

return true;

}

}

4      尝试用Stat获取子节点个数

代码中使用getChildren获取子节点列表,然后统计个数。ZooKeeper还有另一个方法也能获取子节点数:org.apache.zookeeper.data.Stat#numChildren。

将代码leave修改为

String nodeName = tableSerial + "/" + customerName;

log.info("{}: 已经吃完,准备离席,删除节点 {}", customerName, nodeName);

zk.delete(nodeName, 0);

while (true) {

// 使用Stat判断子节点个数

Stat tableStat = new Stat();

zk.getData(tableSerial, true, tableStat);

if (tableStat.getNumChildren() > 0) {

log.info("{}: 还有 {} 人没吃完,你们吃快点...", customerName, tableStat.getNumChildren());

synchronized (mutex) {

mutex.wait();

}

} else {

log.info("{}: 所有人都吃完了,准备散伙", customerName);

return true;

}

}

运行后发现:能够读出子节点个数,但再也无法监听 EventType.NodeChildrenChanged事件,这是ZooKeeper的监听机制决定的。网上搜索到 https://my.oschina.net/u/587108/blog/484203 有介绍,可以自己看一下。简单说就是:

getData()和exists()会监听节点自己的NodeCreated、NodeDeleted、NodeDataChanged事件;getChildren()会监听节点的NodeChildrenChanged事件。

5      完整源码

这个例子没有使用main()函数,改为创建一个 testng 测试用例启动。

5.1   ZooKeeperBarrier.java

package tech.codestory.zookeeper.barrier;

import java.io.IOException;

import java.util.List;

import java.util.concurrent.CountDownLatch;

import org.apache.zookeeper.*;

import org.apache.zookeeper.ZooDefs.Ids;

import org.apache.zookeeper.data.Stat;

import org.slf4j.profiler.Profiler;

import lombok.extern.slf4j.Slf4j;

/**

* @author junyongliao

* @date 2019/8/13

* @since 1.0.0

*/

@Slf4j

public class ZooKeeperBarrier implements Watcher {

/** 等待连接建立成功的信号 */

CountDownLatch connectedSemaphore = new CountDownLatch(1);

/** ZooKeeper 客户端 static */

ZooKeeper zk = null;

/** 子节点发生变化的信号 static */

Integer mutex;

/** 避免重复构建餐桌 */

static Integer tableSerialInitial = Integer.valueOf(1);

/** 餐桌容量 */

int tableCapacity;

/** 餐桌编号 */

String tableSerial;

/** 客人姓名 */

String customerName;

/**

* 构造函数,用于创建zk客户端,以及记录记录barrier的名称和容量

*

* @param address ZooKeeper服务器地址

* @param tableSerial 餐桌编号

* @param tableCapacity 餐桌容量

* @param customerName 客人姓名

*/

ZooKeeperBarrier(String address, String tableSerial, int tableCapacity, String customerName) {

this.tableSerial = tableSerial;

this.tableCapacity = tableCapacity;

this.customerName = customerName;

try {

Profiler profiler = new Profiler(customerName + " 连接到ZooKeeper");

profiler.start("开始连接");

zk = new ZooKeeper(address, 3000, this);

profiler.start("等待连接成功的Event");

connectedSemaphore.await();

profiler.stop();

profiler.setLogger(log);

profiler.log();

mutex = Integer.valueOf(-1);

} catch (IOException e) {

log.error("IOException", e);

zk = null;

} catch (InterruptedException e) {

log.error("InterruptedException", e);

}

synchronized (tableSerialInitial) {

// 创建 tableSerial 的zNode

try {

Stat existsStat = zk.exists(tableSerial, false);

if (existsStat == null) {

this.tableSerial = zk.create(tableSerial, new byte[0], Ids.OPEN_ACL_UNSAFE,

CreateMode.PERSISTENT);

}

} catch (KeeperException e) {

log.error("KeeperException", e);

} catch (InterruptedException e) {

log.error("InterruptedException", e);

}

}

}

@Override

public void process(WatchedEvent event) {

if (Event.EventType.None.equals(event.getType())) {

// 连接状态发生变化

if (Event.KeeperState.SyncConnected.equals(event.getState())) {

// 连接建立成功

connectedSemaphore.countDown();

}

} else if (Event.EventType.NodeChildrenChanged.equals(event.getType())) {

log.info("{} 接收到了通知 : {}", customerName, event.getType());

// 子节点有变化

synchronized (mutex) {

mutex.notify();

}

}

}

/**

* 客人坐在饭桌上

*

* @return 当等到餐桌坐满时返回 true

* @throws KeeperException

* @throws InterruptedException

*/

boolean enter() throws KeeperException, InterruptedException {

String nodeName = tableSerial + "/" + customerName;

log.info("{}: 自己坐下来 {}", customerName, nodeName);

// 属于客人自己的节点,如果会话结束没删掉会自动删除

zk.create(nodeName, new byte[0], Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);

while (true) {

synchronized (mutex) {

// 读出子节点列表,并继续监听

List<String> list = zk.getChildren(tableSerial, true);

if (list.size() < tableCapacity) {

log.info("{}: 当前人数 = {} , 总人数 = {}, 人还不够: 吃饭不积极,一定有问题...", customerName,

list.size(), tableCapacity);

mutex.wait();

} else {

log.info("{}: 人终于够了,开饭...", customerName);

return true;

}

}

}

}

/**

* 客人吃完饭了,可以离开

*

* @return 所有客人都吃完,再返回true

* @throws KeeperException

* @throws InterruptedException

*/

boolean leave() throws KeeperException, InterruptedException {

String nodeName = tableSerial + "/" + customerName;

log.info("{}: 已经吃完,准备离席,删除节点 {}", customerName, nodeName);

zk.delete(nodeName, 0);

while (true) {

// 读出子节点列表,并继续监听

List<String> list = zk.getChildren(tableSerial, true);

if (list.size() > 0) {

log.info("{}: 还有 {} 人没吃完,你们吃快点...", customerName, list.size());

synchronized (mutex) {

mutex.wait();

}

} else {

log.info("{}: 所有人都吃完了,准备散伙", customerName);

return true;

}

}

}

}

5.2   ZooKeeperBarrierTest.java

package tech.codestory.zookeeper.barrier;

import lombok.extern.slf4j.Slf4j;

import org.apache.zookeeper.KeeperException;

import org.testng.annotations.Test;

import java.security.SecureRandom;

import java.util.Random;

import java.util.concurrent.CountDownLatch;

import static org.testng.Assert.*;

/**

* 测试 ZooKeeperBarrier

*

* @author code story

* @date 2019/8/15

*/

@Slf4j

public class ZooKeeperBarrierTest {

Random random = new SecureRandom();

@Test

public void testBarrierTest() {

/** 等待连接建立成功的信号 */

String address = "192.168.5.128:2181";

String barrierName = "/table-" + random.nextInt(10);

int barrierSize = 4;

CountDownLatch countDown = new CountDownLatch(barrierSize);

String[] customerNames = {"张三", "李四", "王五", "赵六"};

for (int i = 0; i < barrierSize; i++) {

String customerName = customerNames[i];

new Thread() {

@Override

public void run() {

log.info("{}: 准备吃饭", customerName);

ZooKeeperBarrier barrier =

new ZooKeeperBarrier(address, barrierName, barrierSize, customerName);

try {

boolean flag = barrier.enter();

log.info("{}: 坐在了可以容纳 {} 人的饭桌", customerName, barrierSize);

if (!flag) {

log.info("{}: 想坐在饭桌时出错了", customerName);

}

} catch (KeeperException e) {

log.error("KeeperException", e);

} catch (InterruptedException e) {

log.error("InterruptedException", e);

}

// 假装在吃饭,随机时间

randomWait();

// 假装吃完了,离开barrier

try {

barrier.leave();

} catch (KeeperException e) {

log.error("KeeperException", e);

} catch (InterruptedException e) {

log.error("InterruptedException", e);

}

countDown.countDown();

}

}.start();

// 等一会儿再开始下一个进程

randomWait();

}

try {

countDown.await();

log.info("这一桌吃完了,散伙");

} catch (InterruptedException e) {

log.error("InterruptedException", e);

}

}

/** 随机等待 */

private void randomWait() {

int r = random.nextInt(100);

for (int j = 0; j < r; j++) {

try {

Thread.sleep(100);

} catch (InterruptedException e) {

log.error("InterruptedException", e);

}

}

}

}

6      测试日志

如下是测试日志

33:34.198 [INFO] ZooKeeperBarrierTest.run(36) 张三: 准备吃饭

33:40.497 [INFO] ZooKeeperBarrierTest.run(36) 李四: 准备吃饭

33:43.333 [DEBUG] ZooKeeperBarrier.log(201)

+ Profiler [张三 连接到ZooKeeper]

|-- elapsed time                   [开始连接]    71.684 milliseconds.

|-- elapsed time           [等待连接成功的Event]  9046.279 milliseconds.

|-- Total               [张三 连接到ZooKeeper]  9118.483 milliseconds.

33:43.346 [INFO] ZooKeeperBarrier.enter(110) 张三: 自己坐下来 /table-2/张三

33:43.353 [INFO] ZooKeeperBarrier.enter(118) 张三: 当前人数 = 1 , 总人数 = 4, 人还不够: 吃饭不积极,一定有问题...

33:49.515 [DEBUG] ZooKeeperBarrier.log(201)

+ Profiler [李四 连接到ZooKeeper]

|-- elapsed time                   [开始连接]     4.365 milliseconds.

|-- elapsed time           [等待连接成功的Event]  9011.503 milliseconds.

|-- Total               [李四 连接到ZooKeeper]  9015.873 milliseconds.

33:49.520 [INFO] ZooKeeperBarrier.enter(110) 李四: 自己坐下来 /table-2/李四

33:49.528 [INFO] ZooKeeperBarrier.process(93) 张三 接收到了通知 : NodeChildrenChanged

33:49.528 [INFO] ZooKeeperBarrier.enter(118) 李四: 当前人数 = 2 , 总人数 = 4, 人还不够: 吃饭不积极,一定有问题...

33:49.532 [INFO] ZooKeeperBarrier.enter(118) 张三: 当前人数 = 2 , 总人数 = 4, 人还不够: 吃饭不积极,一定有问题...

33:50.107 [INFO] ZooKeeperBarrierTest.run(36) 王五: 准备吃饭

33:50.307 [INFO] ZooKeeperBarrierTest.run(36) 赵六: 准备吃饭

33:59.122 [DEBUG] ZooKeeperBarrier.log(201)

+ Profiler [王五 连接到ZooKeeper]

|-- elapsed time                   [开始连接]     4.956 milliseconds.

|-- elapsed time           [等待连接成功的Event]  9008.505 milliseconds.

|-- Total               [王五 连接到ZooKeeper]  9013.468 milliseconds.

33:59.125 [INFO] ZooKeeperBarrier.enter(110) 王五: 自己坐下来 /table-2/王五

33:59.128 [INFO] ZooKeeperBarrier.process(93) 张三 接收到了通知 : NodeChildrenChanged

33:59.132 [INFO] ZooKeeperBarrier.process(93) 李四 接收到了通知 : NodeChildrenChanged

33:59.133 [INFO] ZooKeeperBarrier.enter(118) 李四: 当前人数 = 3 , 总人数 = 4, 人还不够: 吃饭不积极,一定有问题...

33:59.135 [INFO] ZooKeeperBarrier.enter(118) 王五: 当前人数 = 3 , 总人数 = 4, 人还不够: 吃饭不积极,一定有问题...

33:59.136 [INFO] ZooKeeperBarrier.enter(118) 张三: 当前人数 = 3 , 总人数 = 4, 人还不够: 吃饭不积极,一定有问题...

33:59.335 [DEBUG] ZooKeeperBarrier.log(201)

+ Profiler [赵六 连接到ZooKeeper]

|-- elapsed time                   [开始连接]    10.184 milliseconds.

|-- elapsed time           [等待连接成功的Event]  9014.981 milliseconds.

|-- Total               [赵六 连接到ZooKeeper]  9025.175 milliseconds.

33:59.339 [INFO] ZooKeeperBarrier.enter(110) 赵六: 自己坐下来 /table-2/赵六

33:59.343 [INFO] ZooKeeperBarrier.process(93) 张三 接收到了通知 : NodeChildrenChanged

33:59.345 [INFO] ZooKeeperBarrier.enter(122) 赵六: 人终于够了,开饭...

33:59.346 [INFO] ZooKeeperBarrierTest.run(41) 赵六: 坐在了可以容纳 4 人的饭桌

33:59.346 [INFO] ZooKeeperBarrier.process(93) 王五 接收到了通知 : NodeChildrenChanged

33:59.346 [INFO] ZooKeeperBarrier.process(93) 李四 接收到了通知 : NodeChildrenChanged

33:59.348 [INFO] ZooKeeperBarrier.enter(122) 王五: 人终于够了,开饭...

33:59.348 [INFO] ZooKeeperBarrierTest.run(41) 王五: 坐在了可以容纳 4 人的饭桌

33:59.350 [INFO] ZooKeeperBarrier.enter(122) 李四: 人终于够了,开饭...

33:59.350 [INFO] ZooKeeperBarrierTest.run(41) 李四: 坐在了可以容纳 4 人的饭桌

33:59.352 [INFO] ZooKeeperBarrier.enter(122) 张三: 人终于够了,开饭...

33:59.352 [INFO] ZooKeeperBarrierTest.run(41) 张三: 坐在了可以容纳 4 人的饭桌

33:59.646 [INFO] ZooKeeperBarrier.leave(138) 赵六: 已经吃完,准备离席,删除节点 /table-2/赵六

33:59.650 [INFO] ZooKeeperBarrier.process(93) 赵六 接收到了通知 : NodeChildrenChanged

33:59.651 [INFO] ZooKeeperBarrier.process(93) 张三 接收到了通知 : NodeChildrenChanged

33:59.652 [INFO] ZooKeeperBarrier.leave(144) 赵六: 还有 3 人没吃完,你们吃快点...

33:59.652 [INFO] ZooKeeperBarrier.process(93) 李四 接收到了通知 : NodeChildrenChanged

33:59.652 [INFO] ZooKeeperBarrier.process(93) 王五 接收到了通知 : NodeChildrenChanged

33:59.654 [INFO] ZooKeeperBarrier.leave(144) 赵六: 还有 3 人没吃完,你们吃快点...

34:04.356 [INFO] ZooKeeperBarrier.leave(138) 王五: 已经吃完,准备离席,删除节点 /table-2/王五

34:04.361 [INFO] ZooKeeperBarrier.process(93) 赵六 接收到了通知 : NodeChildrenChanged

34:04.363 [INFO] ZooKeeperBarrier.leave(144) 王五: 还有 2 人没吃完,你们吃快点...

34:04.363 [INFO] ZooKeeperBarrier.leave(144) 赵六: 还有 2 人没吃完,你们吃快点...

34:05.958 [INFO] ZooKeeperBarrier.leave(138) 张三: 已经吃完,准备离席,删除节点 /table-2/张三

34:05.963 [INFO] ZooKeeperBarrier.process(93) 王五 接收到了通知 : NodeChildrenChanged

34:05.961 [INFO] ZooKeeperBarrier.leave(138) 李四: 已经吃完,准备离席,删除节点 /table-2/李四

34:05.967 [INFO] ZooKeeperBarrier.leave(144) 张三: 还有 1 人没吃完,你们吃快点...

34:05.968 [INFO] ZooKeeperBarrier.process(93) 赵六 接收到了通知 : NodeChildrenChanged

34:05.971 [INFO] ZooKeeperBarrier.process(93) 张三 接收到了通知 : NodeChildrenChanged

34:05.973 [INFO] ZooKeeperBarrier.leave(149) 赵六: 所有人都吃完了,准备散伙

34:05.981 [INFO] ZooKeeperBarrier.leave(149) 王五: 所有人都吃完了,准备散伙

34:05.982 [INFO] ZooKeeperBarrier.leave(149) 张三: 所有人都吃完了,准备散伙

34:05.983 [INFO] ZooKeeperBarrier.leave(149) 李四: 所有人都吃完了,准备散伙

34:05.985 [INFO] ZooKeeperBarrierTest.testBarrierTest(72) 这一桌吃完了,散伙

ZooKeeper实现同步屏障(Barrier)的更多相关文章

  1. Java并发编程的艺术(八)——闭锁、同步屏障、信号量详解

    1. 闭锁:CountDownLatch 1.1 使用场景 若有多条线程,其中一条线程需要等到其他所有线程准备完所需的资源后才能运行,这样的情况可以使用闭锁. 1.2 代码实现 // 初始化闭锁,并设 ...

  2. Java多线程之CountDownLatch和CyclicBarrier同步屏障的使用

      转载请注明原文地址:http://www.cnblogs.com/ygj0930/p/6558349.html  一:CountDownLatch CountDownLatch是一个执行 完成任务 ...

  3. 并发工具类(二)同步屏障CyclicBarrier

    前言   JDK中为了处理线程之间的同步问题,除了提供锁机制之外,还提供了几个非常有用的并发工具类:CountDownLatch.CyclicBarrier.Semphore.Exchanger.Ph ...

  4. Java并发(十三):并发工具类——同步屏障CyclicBarrier

    先做总结 1.CyclicBarrier 是什么? CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier).它要做的事情是,让一组线程到达一个屏障(也可以叫同步点) ...

  5. zookeeper应用:屏障、队列、分布式锁

    zookeeper工具类: 获取连接实例:创建节点:获取子节点:设置节点数据:获取节点数据:访问控制等. package org.windwant.zookeeper; import org.apac ...

  6. Java并发编程的艺术(九)——闭锁、同步屏障和信号量

    闭锁:CountDownLatch 使用场景 当前线程需要等待若干条线程执行完毕后,才能继续执行的情况. 也可以是若干个步骤执行完毕后的情况. 使用方法 初始化闭锁的时候,填入计数值,然后等待其他线程 ...

  7. Java--CyclicBarrier同步屏障原理,使用

    package com; import java.util.Map; import java.util.concurrent.BrokenBarrierException; import java.u ...

  8. Java核心-多线程-并发控制器-CyclicBarrier同步屏障

    1.基本概念 中文译本同步屏障,同样来自jdk并发工具包中一个并发控制器,它的使用和CountDownLatch有点相似,能够完成某些相同并发场景,但是它们却不相同. 2.抽象模型 主要用来实现多个线 ...

  9. 倒计数锁存器(CountDown Latch)和 CyclicBarrier(同步屏障)

    倒计数锁存器(CountDown Latch)是异常性障碍,允许一个或多个线程等待一个或者多个其他线程来做某些事情. public static long time(Executor executor ...

随机推荐

  1. String到底在内存中是如何存储的

    String会出现在哪些地方 方法内的局部string 类内的字段String static string 容器中存储的string String数组 那么String的位置会影响其存储方式吗? 显然 ...

  2. Centos7 安装jdk,MySQL

    报名立减200元.暑假直降6888. 邀请链接:http://www.jnshu.com/login/1/20535344 邀请码:20535344 学习阿里云平台的云服务器配置Java开发环境.我现 ...

  3. Cookie起源与发展

    上一篇我们在讲优酷弹幕爬虫的时候,引入了一个新的知识点:Cookie,由于篇幅有限当时只是简单的给大家介绍了一下它的作用,今天我们就来全面了解一下Cookie(小饼干)以及相关的知识! 相信很多同学肯 ...

  4. 【学习笔记】动态规划—斜率优化DP(超详细)

    [学习笔记]动态规划-斜率优化DP(超详细) [前言] 第一次写这么长的文章. 写完后感觉对斜优的理解又加深了一些. 斜优通常与决策单调性同时出现.可以说决策单调性是斜率优化的前提. 斜率优化 \(D ...

  5. DAX 第二篇:计算上下文

    计算上下文是计算公式的环境,任何DAX表达式都是在上下文中求值的.行上下文和筛选上下文是DAX中仅有的上下文类型,把这两种上下文称为计算上下文.计算上下文用于限定公式计算的环境,当上下文变化时,相同的 ...

  6. [原创]lvs+ospf+nginx实现高可用大流量web架构

    lvs+ospf+nginx实现高可用大流量web架构配置总概述 架构图: 配置如下: .quagga之zebra配置: # cat /etc/quagga/zebra.conf ! ! Zebra ...

  7. 鸽巢原理及其扩展——Ramsey定理

    第一部分:鸽巢原理 咕咕咕!!! 然鹅大家还是最熟悉我→ a数组:but 我也很重要 $:我好像也出现不少次 以上纯属灌水 文章简叙:鸽巢原理对初赛时的问题求解以及复赛的数论题目都有启发意义.直接的初 ...

  8. web页面保存图片到本地

    web页生成分享海报功能踩坑经验 https://blog.csdn.net/candy_home/article/details/78424642 https://www.jianshu.com/p ...

  9. 跟着大彬读源码 - Redis 6 - 对象和数据类型(下)

    继续撸我们的对象和数据类型. 上节我们一起认识了字符串和列表,接下来还有哈希.集合和有序集合. 1 哈希对象 哈希对象的可选编码分别是:ziplist 和 hashtable. 1.1 ziplist ...

  10. mount命令中offset参数的意义

    mount命令中offset参数的意义        感觉好久没有来写东西了,最近一直忙个不停,今天也一样,总感觉时间不够用,唉,这里来临时总结一下工作中的一点小收获吧.今天要说的是我们常用的解压IM ...