转自:https://www.cnblogs.com/a31415926/p/6744485.html
当一个共享变量被volatile修饰时,它会保证修改的值立即被更新到主存“, 这里的”保证“ 是如何做到的?和 JIT的具体编译后的CPU指令相关吧?
volatile特性
内存可见性:通俗来说就是,线程A对一个volatile变量的修改,对于其它线程来说是可见的,即线程每次获取volatile变量的值都是最新的。
volatile的使用场景
通过关键字sychronize可以防止多个线程进入同一段代码,在某些特定场景中,volatile相当于一个轻量级的sychronize,因为不会引起线程的上下文切换,但是使用volatile必须满足两个条件:
1、对变量的写操作不依赖当前值,如多线程下执行a++,是无法通过volatile保证结果准确性的;
2、该变量没有包含在具有其它变量的不变式中,这句话有点拗口,看代码比较直观。
public class NumberRange { private volatile int lower = 0; private volatile int upper = 10; public int getLower() { return lower; } public int getUpper() { return upper; } public void setLower(int value) { if (value > upper) throw new IllegalArgumentException(...); lower = value; } public void setUpper(int value) { if (value < lower) throw new IllegalArgumentException(...); upper = value; } }
上述代码中,上下界初始化分别为0和10,假设线程A和B在某一时刻同时执行了setLower(8)和setUpper(5),且都通过了不变式的检查,设置了一个无效范围(8, 5),所以在这种场景下,需要通过sychronize保证方法setLower和setUpper在每一时刻只有一个线程能够执行。
下面是我们在项目中经常会用到volatile关键字的两个场景:
1、状态标记量
在高并发的场景中,通过一个boolean类型的变量isopen,控制代码是否走促销逻辑,该如何实现?
public class ServerHandler { private volatile isopen; public void run() { if (isopen) { //促销逻辑 } else { //正常逻辑 } } public void setIsopen(boolean isopen) { this.isopen = isopen } }
场景细节无需过分纠结,这里只是举个例子说明volatile的使用方法,用户的请求线程执行run方法,如果需要开启促销活动,可以通过后台设置,具体实现可以发送一个请求,调用setIsopen方法并设置isopen为true,由于isopen是volatile修饰的,所以一经修改,其他线程都可以拿到isopen的最新值,用户请求就可以执行促销逻辑了。
2、double check
单例模式的一种实现方式,但很多人会忽略volatile关键字,因为没有该关键字,程序也可以很好的运行,只不过代码的稳定性总不是100%,说不定在未来的某个时刻,隐藏的bug就出来了。
class Singleton { private volatile static Singleton instance; public static Singleton getInstance() { if (instance == null) { syschronized(Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
不过在众多单例模式的实现中,我比较推荐懒加载的优雅写法Initialization on Demand Holder(IODH)。
public class Singleton { static class SingletonHolder { static Singleton instance = new Singleton(); } public static Singleton getInstance(){ return SingletonHolder.instance; } }
如何保证内存可见性?
在java虚拟机的内存模型中,有主内存和工作内存的概念,每个线程对应一个工作内存,并共享主内存的数据,下面看看操作普通变量和volatile变量有什么不同:
1、对于普通变量:读操作会优先读取工作内存的数据,如果工作内存中不存在,则从主内存中拷贝一份数据到工作内存中;写操作只会修改工作内存的副本数据,这种情况下,其它线程就无法读取变量的最新值。
2、对于volatile变量,读操作时JMM会把工作内存中对应的值设为无效,要求线程从主内存中读取数据;写操作时JMM会把工作内存中对应的数据刷新到主内存中,这种情况下,其它线程就可以读取变量的最新值。
volatile变量的内存可见性是基于内存屏障(Memory Barrier)实现的,什么是内存屏障?内存屏障,又称内存栅栏,是一个CPU指令。在程序运行时,为了提高执行性能,编译器和处理器会对指令进行重排序,JMM为了保证在不同的编译器和CPU上有相同的结果,通过插入特定类型的内存屏障来禁止特定类型的编译器重排序和处理器重排序,插入一条内存屏障会告诉编译器和CPU:不管什么指令都不能和这条Memory Barrier指令重排序。
代码示例如下:
class Singleton { private volatile static Singleton instance; private int a; private int b; private int b; public static Singleton getInstance() { if (instance == null) { syschronized(Singleton.class) { if (instance == null) { a = 1; // 1 b = 2; // 2 instance = new Singleton(); // 3 c = a + b; // 4 } } } return instance; } }
1、如果变量instance没有volatile修饰,语句1、2、3可以随意的进行重排序执行,即指令执行过程可能是3214或1324。
2、如果是volatile修饰的变量instance,会在语句3的前后各插入一个内存屏障。
通过观察volatile变量和普通变量所生成的汇编代码可以发现,操作volatile变量会多出一个lock前缀指令:
Java代码:
instance = new Singleton();
汇编代码:
0x01a3de1d: movb $0x0,0x1104800(%esi); 0x01a3de24: **lock** addl $0x0,(%esp);
这个lock前缀指令相当于上述的内存屏障,提供了以下保证:
1、将当前CPU缓存行的数据写回到主内存;
2、这个写回内存的操作会导致在其它CPU里缓存了该内存地址的数据无效。
CPU为了提高处理性能,并不直接和内存进行通信,而是将内存的数据读取到内部缓存(L1,L2)再进行操作,但操作完并不能确定何时写回到内存,如果对volatile变量进行写操作,当CPU执行到Lock前缀指令时,会将这个变量所在缓存行的数据写回到内存,不过还是存在一个问题,就算内存的数据是最新的,其它CPU缓存的还是旧值,所以为了保证各个CPU的缓存一致性,每个CPU通过嗅探在总线上传播的数据来检查自己缓存的数据有效性,当发现自己缓存行对应的内存地址的数据被修改,就会将该缓存行设置成无效状态,当CPU读取该变量时,发现所在的缓存行被设置为无效,就会重新从内存中读取数据到缓存中。
相关推荐
6. **多线程**:Java提供了丰富的API支持多线程编程,如Thread类、Runnable接口,以及synchronized关键字、volatile关键字等,理解并发编程的概念和实践方法对于开发高效程序至关重要。 7. **IO流**:Java的IO流...
面试者需要熟悉线程的创建、同步、中断和状态转换,以及synchronized关键字、volatile变量、ThreadLocal、Lock接口等并发工具的用法。面试官可能会设计问题来测试候选人在处理多线程问题时的能力,比如死锁、活锁、...
书中将探讨并发编程的挑战,如线程同步、死锁、活锁等问题,以及如何使用synchronized、volatile关键字来确保线程安全。 3. **异常处理**:Java异常处理机制帮助开发者捕获和处理程序运行时错误。书里会解释如何...
理解synchronized、volatile关键字的作用,以及线程池的使用,能帮助我们编写出安全且高效的并发代码。另外,Java内存模型JMM(Java Memory Model)也需有所了解,它规定了线程间的可见性和内存一致性。 最后,垃圾...
4. **并发编程**:Java提供了丰富的并发工具,如Thread、Runnable、synchronized、volatile、ConcurrentHashMap等,理解它们的工作原理和使用场景是必不可少的。 5. **集合框架**:ArrayList、LinkedList、HashSet...
4. **并发编程**:如何正确地使用synchronized、volatile关键字,理解线程安全问题。 5. **设计模式**:介绍一些常见的设计模式,如工厂模式、单例模式、观察者模式等,以及它们在Java中的应用。 6. **JVM工作原理**...
多线程是Java的一大优势,面试时会涉及线程的创建、同步、并发控制,例如synchronized、volatile、ThreadLocal、Lock接口等。理解并发模型和线程安全问题的处理至关重要。 JVM(Java虚拟机)是Java程序运行的平台,...
面试中会考察线程同步机制,如synchronized关键字、volatile、Lock(ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier)以及线程池的使用。 5. **异常处理**:理解异常的分类,如何正确使用try-catch-...
深入理解并发原理和同步机制(如`synchronized`、`volatile`关键字、`Lock`接口)对于编写高效并发代码至关重要。 4. **集合框架**:Java集合框架是程序中数据存储和操作的核心,包括`List`、`Set`、`Map`等接口...
- **同步机制**:synchronized关键字、 volatile变量和Lock接口,用于保证线程安全。 7. **I/O流** - **字节流与字符流**:理解和使用InputStream/OutputStream和Reader/Writer家族。 - **缓冲流**:提高读写...
例如,线程间的可见性、volatile关键字的作用以及synchronized的使用,都是Java并发编程中的难点。在书中,作者会通过实际的并发puzzlers解释这些问题,配合源码,读者可以更直观地理解线程安全性和并发控制。 异常...
谜题将展示如何正确使用synchronized、volatile关键字,以及如何理解和避免死锁、活锁、饥饿等问题,确保多线程环境下的程序行为是可预测和安全的。 书中还涵盖了Java的IO和NIO系统,解释了流的使用、文件操作以及...
书中可能涵盖线程安全、死锁、活锁、饥饿等问题,以及如何使用`synchronized`、`volatile`关键字和`java.util.concurrent`包来解决这些问题。 3. **内存管理**:Java的垃圾回收机制虽然自动化,但开发者仍需理解...
书中会讨论线程安全、同步机制(如`synchronized`关键字、`volatile`变量、`Lock`接口)、线程池的使用,以及如何处理线程间的通信和协作。 4. **面向对象编程**:Java是纯面向对象的语言,封装、继承和多态是其三...
4. 多线程:并发编程是Java中的一个重要领域,书中可能会介绍线程安全问题,如synchronized关键字的使用,volatile变量的作用,以及死锁、活锁和饥饿现象的预防。 5. 异常处理:Java的异常处理机制是一个重要的知识...
线程同步机制,如synchronized关键字、volatile变量、Lock接口以及条件变量,是确保线程安全的重要工具。 4. **集合框架**:Java集合框架包括List、Set、Queue等接口和ArrayList、LinkedList、HashSet、HashMap等...
- 通过这个谜题,读者将了解volatile关键字的作用及其在多线程环境下的正确使用方法。 #### 结论 《Java Puzzlers: Traps, Pitfalls, and Corner Cases》不仅是一本有趣的编程书籍,更是一本宝贵的参考指南。通过...
文章可能会讲解线程安全、同步机制(如`synchronized`关键字和`volatile`变量),以及如何避免死锁和活锁。 4. **集合框架**:Java的集合框架是其强大之处,但也容易出错。可能讨论的问题有:选择适当的集合类型...