文章目录
curator-recipes功能简介
curator-recipes包中包含了对zookeeper场景应用场景的封装,好的项目源码让人从包名就能看出其功能,下面先看下recipes的包结构
简单介绍下不同包及其对应功能
包名 | 功能简介 |
---|---|
atomic | 分布式计数器(DistributedAtomicLong),能在分布式环境下实现原子自增 |
barriers | 分布式屏障(DistributedBarrier),使用屏障来阻塞分布式环境中进程的运行,直到满足特定的条件 |
cache | 监听机制,分为NodeCache(监听节点数据变化),PathChildrenCache(监听节点的子节点数据变化),TreeCache(既能监听自身节点数据变化也能监听子节点数据变化) |
leader | leader选举 |
locks | 分布式锁 |
nodes | 提供持久化节点(PersistentNode)服务,即使客户端与zk服务的连接或者会话断开 |
queue | 分布式队列(包括优先级队列DistributedPriorityQueue,延迟队列DistributedDelayQueue等) |
shared | 分布式计数器SharedCount |
在介绍curator的分布式原子计数器之前,先抛出一个经典的面试问题,i++在多线程环境下是否存在问题?
先来看一段测试代码,其中CountDownLatch是为了让主线程阻塞直到所有子线程执行完,其应用场景如下:CountDownLatch调用await()方法的线程将一直阻塞等待,直到这个CountDownLatch对象的计数值减到0(每次调用countDown方法计数器减一)为止。例子里每个子线程自增100000次后调用countDown()方法将计数器减一,初始化数值10,10个线程全部跑完自增后,主线程await方法不再阻塞,输出count值
static int count = 0;
static CountDownLatch countDownLatch = new CountDownLatch(10);
public static void main(String[] args) {
//创建10个线程 每个线程内部自增100000次
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 100000; j++) {
count++;
}
countDownLatch.countDown();
}).start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count);
}
执行几次后,发现结果不总是10*100000,故可以证明i++确实存在线程安全问题
Java中i++自增线程安全问题的由来
先来了解下几个基本概念
Java内存模型
- Java内存模型规定所有的变量都是存在主存中
- 每个线程都有自己的工作内存
- 线程对变量的操作都必须在线程内部工作内存中进行,不能直接对主存中变量进行操作
网上一张图描绘的很形象
现在可以联想到i++操作,线程对共享变量的修改总是分为 读-改-写
这3步,即
- 线程先从主存中读取变量到本地工作内存中(读)
- 线程在本地工作内存中对变量进行+1操作(改)
- 线程将本地内存中变量的值写回主存(写)
再想,多线程又是可以并行执行的
假设初始count值为0,如果n个线程同时读到了0这个值,那么更新后count值是多少?
这就是i++出现线程安全问题的核心原因 ,i++操作分为三步,其中只有读取和写入操作是原子的,读改写三步一起执行时,无法保证原子性
原子性
原子性就是指该操作是不可再分的,要么全部执行,要么全部不执行。最经典的就是银行转账问题,对应到i++问题中就是上面所述的:只有读取和写入操作是原子的,而读改写合并操作不能保证原子性。
Java中保证原子性操作的有:
- Synchronized和Lock,加锁使得同一时刻只有一个线程能访问共享变量,操作自然是原子的
- java.util.concurrent.atomic下的原子操作类,如AtomicInteger,AtomicReference,基于Cas算法实现了类似乐观锁版本更新控制的原子操作
这两种方法,下面会分别介绍如何使用来解决i++原子性问题
内存可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改后变量的值
Java里提供了volatile关键字来修饰变量,使得线程对一个变量值的修改会立马更新到主存之中,其它线程会重新从主存中去获取最新的变量值。
那么volatile关键字是如何使得其它线程能立马获取到最新的变量值呢?
如果对声明了Volatile变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是就算写回到内存,如果其他(线程)处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里
核心就是两点
- Lock前缀指令会引起处理器缓存回写到内存
- 一个处理器的缓存回写到内存会导致其他处理器的缓存无效(嗅探机制)
那么问题来了,volatile关键字是否能解决i++自增的线程安全问题呢?不是每个线程修改后的值都能立马被其他线程看到么?
看官们自己先思考3分钟?
举个例子,假设volatile变量count初始