Java 提供了一种较为轻量级的可见性和有序性问题的解决方案,那就是使用 volatile 关键字。由于使用 volatile 不会引起上下文的切换和调度,所以 volatile 对性能的影响较小,开销较低。
从并发三要素的角度看,volatile 可以保证其修饰的变量的可见性和有序性,无法保证原子性(不能保证完全的原子性,只能保证单次读/写操作具有原子性,即无法保证复合操作的原子性)
volatile 如何保证可见性和有序性?
在图中,assign 操作,此时如果这个 flag 变量是加了 volatile 关键字的话,那么此时,就是一定会强制保证说assign 之后,就立马执行 store +write,刷回到主内存里去。从而保证只要工作内存一旦变为 flag=1,主内存立马变成lag=1。
此外,如果这个变量被 volatile 修饰,其他线程的工作内存中的flag变量的缓存,会过期。线程2如果再从工作内存里读取 fag 变量的信,发现他已经过期了,此时就会重新从主内存里来加载这个flag=1。
内存屏障
LoadLoad 屏障: Load1; LodLoad; Load2,确保 Load1 数据的装载先于 Load2 后所有装载指令,也就是,Load1对应的代码和 Load2 对应的代码,是不能指令重排的;
StoreStore 屏障:Storel; Storetore; Store2,确保 Store1的数据一定刷回主存,对其他cpu可见,先于 store2以及后续指令;
Loadstore 屏障: Load1,LoadStore; Store2,确保 Load1 指令的数据装载,先于 Store2以及后续指令;
StoreLoad屏障: Store1;StoreLoad; Load2,确保 Store1指令的数据一定刷回主存,对其他cpu可见,先于 Load2以及后续指令的数据装载。
每个volatile 写操作前面,加 Storestore 屏障,禁止上面的普通写和他重排,每个 volatile写操作后面,加 StoreLoad 屏障,禁止跟下面的volatile 读/写重排;
每个volatile读操作后面,加 LoadLoad屏障,禁上下面的普通读和voaltile读重排;每个 volatile读操作后面,加 Loadstore 屏障,禁止下面的普通写和 volatile 读重排。
volatile 为何不能保证原子性?
上图:
1、如果线程1与线程而同时从主存中读取flag(flag=0)变量的值,进去到线程内;
2、线程1,执行i++,并发flag置为1,立马将flag写入到主存中。但与此同时也完成了flag=1操作的时候,这时候volatile无法保证原子性。
volatile运用案例
ZooKeeper 是一个分布式协调服务,其中 volatile
关键字常常用于保证共享状态的可见性,特别是在实现分布式选举等功能时。以下是 ZooKeeper 中使用 volatile
的一个简化实例代码,来讲解其运用:
首先,我们创建一个简单的分布式选举的类 LeaderElection
,用于演示 ZooKeeper 中的 volatile
的使用:
import org.apache.zookeeper.*;
public class LeaderElection {
private static final String ZOOKEEPER_ADDRESS = "localhost:2181";
private static final int SESSION_TIMEOUT = 3000;
private ZooKeeper zooKeeper;
private volatile boolean isLeader;
public LeaderElection() {
try {
this.zooKeeper = new ZooKeeper(ZOOKEEPER_ADDRESS, SESSION_TIMEOUT, new Watcher() {
@Override
public void process(WatchedEvent event) {
// 处理ZooKeeper连接事件
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
public void runForLeader() throws KeeperException, InterruptedException {
try {
// 在ZooKeeper上创建临时节点作为候选人
String znodePath = zooKeeper.create("/election/candidate", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
// 获取所有候选人节点
var candidates = zooKeeper.getChildren("/election", false);
int smallestSeqNum = Integer.MAX_VALUE;
// 找出最小序列号的候选人节点,成为领导者
for (String candidate : candidates) {
int seqNum = Integer.parseInt(candidate.substring(10));
if (seqNum < smallestSeqNum) {
smallestSeqNum = seqNum;
}
}
if (smallestSeqNum == Integer.parseInt(znodePath.substring(10))) {
isLeader = true;
}
} catch (Exception e) {
e.printStackTrace();
}
}
public boolean isLeader() {
return isLeader;
}
public static void main(String[] args) throws KeeperException, InterruptedException {
LeaderElection leaderElection = new LeaderElection();
leaderElection.runForLeader();
if (leaderElection.isLeader()) {
System.out.println("I am the leader.");
} else {
System.out.println("I am a follower.");
}
// 假设这里持续运行其他逻辑,保持ZooKeeper连接和节点有效性
Thread.sleep(Long.MAX_VALUE);
}
}
在上面的代码中,LeaderElection
类负责实现一个分布式选举过程,以确定哪个节点成为领导者。在该实例中,我们使用了一个 volatile
变量 isLeader
,用于标识当前节点是否成为领导者。
在 runForLeader()
方法中,我们通过在 ZooKeeper 上创建临时顺序节点 /election/candidate
,并获取所有候选人节点,找出序列号最小的候选人节点成为领导者。节点的序列号通过截取节点路径字符串的后缀(例如,/election/candidate0000000010
的序列号是 10
)来获取。
注意,在实际分布式系统中,选举算法可能更加复杂,并且可能涉及更多的节点和节点状态处理。此处的示例代码仅用于说明 volatile
关键字在 ZooKeeper 中的简单应用场景。