深入理解关键字 volatile

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 中的简单应用场景。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值