乐趣区

关于java:彻底理解-volatile-关键字及应用场景面试必问小白都能看懂

起源: blog.csdn.net/fumitzuki/article/details/81630048

volatile 关键字是由 JVM 提供的最轻量级同步机制。与被滥用的 synchronized 不同,咱们并不习惯应用它。想要正确且齐全的了解它并不容易。

Part1Java 内存模型

Java 内存模型由 Java 虚拟机标准定义,用来屏蔽各个平台的硬件差别。简略来说:

  • 所有变量贮存在主内存。
  • 每条线程领有本人的工作内存,其中保留了主内存中线程应用到的变量的正本。
  • 线程不能间接读写主内存中的变量,所有操作均在工作内存中实现。线程,主内存,工作内存的交互关系如图。

内存间的交互操作有很多,和 volatile 无关的操作为:

  • read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的 load 动作应用
  • load(载入):作用于工作内存的变量,它把 read 操作从主内存中失去的变量值放入工作内存的变量正本中。
  • use(应用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个须要应用变量的值的字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接管到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的 write 的操作。
  • write(写入):作用于主内存的变量,它把 store 操作从工作内存中一个变量的值传送到主内存的变量中。

对被 volatile 润饰的变量进行操作时,须要满足以下规定:

  • 规定 1:线程对变量执行的前一个动作是 load 时能力执行 use,反之只有后一个动作是 use 时能力执行 load。线程对变量的 read,load,use 动作关联,必须间断一起呈现。—– 这保障了线程每次应用变量时都须要从主存拿到最新的值,保障了其余线程批改的变量本线程能看到。
  • 规定 2:线程对变量执行的前一个动作是 assign 时能力执行 store,反之只有后一个动作是 store 时能力执行 assign。线程对变量的 assign,store,write 动作关联,必须间断一起呈现。—– 这保障了线程每次批改变量后都会立刻同步回主内存,保障了本线程批改的变量其余线程能看到。
  • 规定 3:有线程 T,变量 V、变量 W。假如动作 A 是 T 对 V 的 use 或 assign 动作,P 是依据规定 2、3 与 A 关联的 read 或 write 动作;动作 B 是 T 对 W 的 use 或 assign 动作,Q 是依据规定 2、3 与 B 关联的 read 或 write 动作。如果 A 先与 B,那么 P 先与 Q。—— 这保障了 volatile 润饰的变量不会被指令重排序优化,代码的执行程序与程序的程序雷同。

Part2 应用 volatile 关键字的个性

被 volatile 润饰的变量保障对所有线程可见。

由上文的规定 1、2 可知,volatile 变量对所有线程是立刻可见的,在各个线程中不存在一致性问题。那么,咱们是否能得出结论:volatile 变量在并发运算下是线程平安的呢?这的确是一个十分常见的误会,写个简略的例子:

public class VolatileTest extends Thread{
    static volatile int increase = 0;
    static AtomicInteger aInteger=new AtomicInteger();// 对照组
    static void increaseFun() {
        increase++;
        aInteger.incrementAndGet();}
    public void run(){
        int i=0;
        while (i < 10000) {increaseFun();
            i++;
        }
    }
    public static void main(String[] args) {VolatileTest vt = new VolatileTest();
        int THREAD_NUM = 10;
        Thread[] threads = new Thread[THREAD_NUM];
        for (int i = 0; i < THREAD_NUM; i++) {threads[i] = new Thread(vt, "线程" + i);
            threads[i].start();}
        //idea 中会返回主线程和守护线程,如果用 Eclipse 的话改为 1
        while (Thread.activeCount() > 2) {Thread.yield();
        }
        System.out.println("volatile 的值:"+increase);
        System.out.println("AtomicInteger 的值:"+aInteger);
    }
}

这个程序咱们跑了 10 个线程同时对 volatile 润饰的变量进行 10000 的自增操作 (AtomicInteger 实现了原子性,作为对照组),如果 volatile 变量是并发平安的话,运行后果应该为 100000,可是屡次运行后,每次的后果均小于预期值。显然上文的说法是有问题的。

volatile 润饰的变量并不保值原子性,所以在上述的例子中,用 volatile 来保障线程平安不靠谱。咱们用 Javap 对这段代码进行反编译,为什么不靠谱几乎高深莫测:

getstatic 指令把 increase 的值拿到了操作栈的顶部,此时因为 volatile 的规定,该值是正确的。

iconst_1 和 iadd 指令在执行的时候 increase 的值很有可能曾经被其余线程加大,此时栈顶的值过期。

putstatic 指令接着把过期的值同步回主存,导致了最终后果较小。

volatile 关键字只保障可见性,所以在以下状况中,须要应用锁来保障原子性:

  • 运算后果依赖变量的以后值,并且有不止一个线程在批改变量的值。
  • 变量须要与其余状态变量独特参加不变束缚

那么 volatile 的这个个性的应用场景是什么呢?

  • 模式 1:状态标记
  • 模式 2:独立察看(independent observation)
  • 模式 3:“volatile bean”模式
  • 模式 4:开销较低的“读-写锁”策略

具体场景 — blog.csdn.net/vking_wang/article/details/9982709

禁止指令重排序优化。

由上文的规定 3 可知,volatile 变量的第二个语义是禁止指令重排序。指令重排序是什么?简略点说就是 jvm 会把代码中没有依赖赋值的中央打乱执行程序,因为一些规定限定,咱们在单线程内察看不到打乱的景象(线程内体现为串行的语义),然而在并发程序中,从别的线程看另一个线程,操作是无序的。一个十分经典的指令重排序例子:

public class SingletonTest {
    private volatile static SingletonTest instance = null;
    private SingletonTest() {}
    public static SingletonTest getInstance() {if(instance == null) {synchronized (SingletonTest.class){if(instance == null) {instance = new SingletonTest();  // 非原子操作
                }
            }
        }
        return instance;
    }
}

这是单例模式中的“双重查看加锁模式”,咱们看到 instance 用了 volatile 润饰,因为 instance = new SingletonTest(); 可分解为:

  1. memory =allocate(); // 调配对象的内存空间
  2. ctorInstance(memory); // 初始化对象
  3. instance =memory; // 设置 instance 指向刚调配的内存地址

操作 2 依赖 1,然而操作 3 不依赖 2,所以有可能呈现 1,3,2 的程序,当呈现这种程序的时候,尽管 instance 不为空,然而对象也有可能没有正确初始化,会出错。

Part3 总结

并发三特色可见性和有序性和原子性中,volatile

通过新值立刻同步到主内存和每次应用前从主内存刷新机制保障了可见性。

通过禁止指令重排序保障了有序性。

无奈保障原子性。

而咱们晓得,synchronized 关键字

通过 lock 和 unlock 操作保障了原子性,

通过对一个变量 unlock 前,把变量同步回主内存中保障了可见性,

通过一个变量在同一时刻只容许一条线程对其进行 lock 操作保障了有序性。

他的“万能”也间接导致了咱们对 synchronized 关键字的滥用,越泛用的管制,对性能的影响也越大,尽管 jvm 一直的对 synchronized 关键字进行各种各样的优化,然而咱们还是要在适合的时候想起 volatile 关键字啊,哈哈哈哈。

参考资料:

<< 深刻了解 Java 虚拟机 >> 周志明 著 \
https://blog.csdn.net/chunyua…\
https://blog.csdn.net/vking_w…

近期热文举荐:

1.1,000+ 道 Java 面试题及答案整顿 (2022 最新版)

2. 劲爆!Java 协程要来了。。。

3.Spring Boot 2.x 教程,太全了!

4. 别再写满屏的爆爆爆炸类了,试试装璜器模式,这才是优雅的形式!!

5.《Java 开发手册(嵩山版)》最新公布,速速下载!

感觉不错,别忘了顺手点赞 + 转发哦!

退出移动版