文章目录
1、相关概念
- 原子性:执行一个或者多个操作的时候,要么全部执行,要么都不执行,并且中间过程中不会被打断。独占锁和 CAS 能够保证原子性。
- 可见性:指多线程访问同一个变量的时候,一个线程修改了变量的值,其他线程能够立刻看得到修改的值。锁和 volatile 能够保证可见性。
- 有序性:程序执行的顺序按照代码先后的顺序执行。锁和 volatile 能够保证有序性。
2、volatile 的作用
2.1 保证可见性
- 保证被修改共享变量的的可见性
- 即多个线程操作读写时,能被其他线程感知到
在读取共享变量时会强制将主存中的变量值读取到自己的本地内存中
写入变量后又会强制将自己的新值刷新到主内存
其实这就是 JMM 内存模型
2.2 防止指令重排序
- 在双重判空加锁的单例模式中,instance 必须用 volatile 修饰,原因是 Singleton instance = new Singleton 时,一般有三个步骤
- 分配内存空间
- 初始化 Singleton 对象
- 将内存地址赋值给 instance
- JVM 虚拟机在执行的时候可能会做指令重排序优化,2和3的顺序颠倒,
在这种情况下,第一层判空会获取一个没有初始化完毕的实例,从而发生空指针异常 - 而 volatile 可以阻止这种情况的发生
2.3 为什么会发送指令重排序
- 重排序指的是:编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。
- 如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性
- 数据依赖分为下列3种类型:
- 上面情况,只要重排序两个操作的执行顺序,程序的执行结果就会被改变。
- 而编译器和处理器可能会对操作做重排序,但是编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。
3、synchronized 的作用
3.1 保证原子性
- Java内存模型提供了字节码指令 monitorenter 和 monitorexit 来隐式的使用这两个操作,在 synchronized 块之间的操作是具备原子性的。
- 线程在执行 monitorenter 指令的时候,会对 Monitor 进行加锁,加锁后其他线程无法获得锁,除非线程主动解锁。
- 即使在执行过程中,由于某种原因,比如 CPU 时间片用完,线程放弃了 CPU,但是它并没有进行解锁。
- 而由于 synchronized 的锁是可重入的,下一个时间片还是只能被它自己获取到,还是会继续执行代码。直到所有代码执行完。
- 这就保证了原子性。
3.2 保证有序性
synchronized 无法禁止指令重排序和处理器优化,那它如何保证有序性?
- 这和 as-if-serial 语义有关
- as-if-serial 语义:不管怎么重排序,单线程程序的执行结果都不能被改变
- 编译器和处理器无论如何优化,都必须遵守 as-if-serial 语义。只要编译器和处理器都遵守了这个语义,那么就可以认为单线程程序是按照顺序执行的
- 因为 synchronized 修饰的代码,同一时间是单线程执行的,所以保证了有序性
3.3 保证可见性
- synchronized 在修改了本地内存中的变量后,解锁前会将本地内存修改的值刷新到主内存中,确保了共享变量的值是最新的。
- 解锁后,后续其他线程获取到的就是被修改后的最新值,也就保证了可见性
4、volatile 和 synchronized 的区别
- synchronized 是一种锁机制,会消耗性能,产生阻塞;但 volatile 是基于内存屏障实现的,不会有 synchronized 带来的性能消耗和阻塞问题
- volatile 还能禁止指令重排序
5、参考资料
volatile和synchronized特点
深入解析synchronized实现原理,如何保证原子性、有序性和可见性?
为什么synchronized无法禁止指令重排,却能保证有序性?
https://blog.csdn.net/mifffy_java/article/details/99944946