并发编程Part 2
1. JMM
问题:请你谈谈你对volatile的理解?
volitile 是 Java 虚拟机提供的一种轻量级的同步机制 ,三大特性:
- 保证可见性
- 不保证原子性
- 禁止指令重排
线程之间如何通信?
- 通信是指线程之间以如何来交换信息。
- 一般线程之间的通信机制有两种:共享内存和消息传递。
- Java的并发采用的是共享内存模型~
什么是JMM?
- Java内存模型即JMM(Java Memeory Model),它并不是真实存在的,而是Java中抽象出来的一个概念,用于多线程场景。
共享内存模型 指的就是 Java内存模型 ( 简称 JMM) { Java多线程内存模型 }- JMM 本身是一种抽象的概念,并不真实存在,它描述的是一组规则或者规范~
- JMM描述了一组规则或规范,定义了一个线程对共享变量写入时对另一个线程是可见的,从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系
- 关于JMM的一些同步的约定:
- 线程解锁前,必须把共享变量立刻刷回主存。
- 线程加锁前,必须读取主存中的最新值到工作内存中!
- 加锁和解锁必须是同一把锁
- 在JMM中把多个线程间通信的共享内存称之为主内存{Main Memory},线程之间的共享变量存储在主内存(main memory)中,而在并发编程中每个线程都维护自己的一个本地内存{工作内存},每个线程都有一个私有的本地内存(Local Memory),其中保存的数据是从主内存中拷贝过来的数据副本,而JMM主要是控制本地内存和主内存之间的数据交互;
本地内存是 JMM 的一个抽象概念,并不真实存在。![]()
JMM内存模型 从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:
- 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
- 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。
下面通过示意图来说明线程之间的通信:
总结:
- 什么是Java内存模型:Java内存模型简称JMM,定义了一个线程对另一个线程可见。
- 共享变量存放在主内存中,每个线程都有自己的本地内存,当多个线程同时访问一个数据的时候,可能本地内存没有及时刷新到主内存,所以就会发生线程安全问题。
怎样保证线程B可以同步感知线程A修改了共享变量呢?
- 使用volatile关键字修饰变量
- 使用synchronized修饰修改变量的方法
- wait/notify
- while轮询
JMM内存模型底层八大原子操作:

2. Volatile
volitile 是 Java 虚拟机提供的一种轻量级的同步机制 ,三大特性:
- 保证可见性
- 不保证原子性
- 禁止指令重排,从而避免多线程环境下程序中出现乱序执行的现象
使用volatile可以保证线程间共享变量的可见性,详细的说就是符合以下两个规则:
- 线程对共享变量进行修改之后,要立刻回写到主内存。
- 线程对共享变量读取的时候,要从主内存中读,而不是缓存。
- 对于可见性,Java提供了volatile关键字来保证多线程共享变量可见性和禁止JVM指令重排,volatile提供happens-before{先行发生}的保证,volatile变量可以确保先行关系,即写操作会发生在后续的读操作之前,确保一个线程的修改能对其它线程是可见的。当一个共享变量被volatile关键字修饰时,它会保证修改的值会立即被更新到主内存中,当有其它线程需要读取时,它会从主内存中读取最新的值。
- {volatile关键字为变量的访问提供了一种免锁机制}
- 但是,volatile并不能保证原子性,例如用volatile修饰count变量,那么count++操作就不是原子性的{因为首先count++本身就不是原子性操作}
- 而java.util.concurrent.atomic包下的原子类提供的atomic方法可以保证对其进行原子性操作,比如AtomicInteger类提供的getAndIncrement()方法会原子性的进行增量操作把当前值加1。
2.2 Volatile为什么可以保证可见性和有序性? - Volatile的底层原理
- 由于底层是通过操作系统的内存屏障来实现的,所以Volatile会禁止指令重排,因此同时也就保证了有序性。
2.3 指令重排序与内存屏障
- 并发编程三大特性:原子性,可见性,有序性
- Volatile保证可见性与有序性,但是不能保证原子性,保证原子性需要借助synchronized这样的锁机制
什么是重排序?
- CPU处理器为了提高程序运行效率,可能会对输入代码进行优化,对指令进行重新排序{重排序},它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
- CPU处理器在进行指令重排序时必须要考虑数据之间的依赖性!{举例:一个后续指令要读取一个前面指令写入的数据,那么CPU处理器会确保这两个指令的顺序保持一致。}
指令重排可能带来的问题:
- 指令重排对单线程运行时不会有任何问题,但是多线程就不一定了,可能会导致多线程程序出现内存可见性、有序性、死锁和活锁问题。
- 可见性问题:指令重排可能导致变量的更新在某个线程中不可见,即一个线程对共享变量的更新操作对其它线程来说不可见,这可能会导致数据不一致性问题。
- 有序性问题:指令重排可能会改变操作的执行顺序,从而引起线程之间的有序性问题。在多线程环境下,线程依赖于特定的操作顺序来保证正确的逻辑和数据一致性,指令重排可能会破坏这种有序性。
- 死锁和活锁问题:指令重排可能导致线程在加锁和解锁操作上出现问题{可能导致锁的获取和释放顺序发生变化},进而引发死锁或活锁。
重排序实际执行的指令步骤 :
- 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 指令级并行的重排序。现代处理器采用了指令级并行技术(ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
重排序会遵循as-if-serial和happens-before原则!
2.4 as-if-serial语义
- as-if-serial语义的意思是:不管怎么重排序{编译器和处理器为了提高并行度},线程执行的结果不能被改变{与单线程执行结果一致}
- 为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器做重排序。
2.5 happens-before原则
从JDK1.5开始,Java开始使用新的JSR-133内存模型,提供了happens-before{先行发生}原则来辅助保证程序执行的原子性、可见性以及有序性的问题,它是判断数据是否存在竞争、线程是否安全的依据,它提供了8组规则,通过这些规则可以确定操作之间是否存在happens-before关系。
happens-before原则内容如下{简单说明四点}:
- 程序的顺序性原则:即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行
- 锁规则:解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)操作之前
- volatile规则:volatile变量的写操作,先发生于读操作,这保证了volatile变量的可见性,简单理解就是:volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能看到该变量的最新值。
- 传递性规则:A先于B,B先于C => 那么A必须先于C
总结:as-if-serial语义和happens-before规则这么做的目的,都是为了在不改变程序执行结果的 前提下,尽可能地提高程序执行的并行度。
2.6 内存屏障
- 内存屏障(Memeory Barrier)是一种与CPU相关的特殊指令{CPU指令}。
- 内存屏障可以避免指令重排、保证内存操作的可见性。
- 在Java中,内存屏障的概念被抽象为不同的同步机制,如volatile关键字和synchronized关键字,当使用这些关键字时,编译器和JVM会插入对应的内存屏障指令,以确保内存操作的有序性和可见性。
- 通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化{禁止上面指令和下面指令顺序交换}。
2.7 synchronized 和 volatile 的区别是什么?
- synchronized 表示同一时间只有一个线程可以获取作用对象的锁,去进入同步代码块或者同步方法来执行代码,其他线程会被阻塞。
- volatile 表示变量在 CPU 的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的可见性;禁止指令重排序。
区别:
- volatile 是变量修饰符,volatile关键字只能用于变量;synchronized 关键字可以修饰方法以及代码块。
- volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证对变量的修改操作的可见性和原子性{加锁和解锁}。
- volatile 不会造成线程的阻塞;synchronized 会造成其它线程的阻塞。
- volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。
补充:临界区{有线程安全问题的代码块}
提问:Volatile的内存屏障在哪个地方使最多?
- 在单例模式里面使用最多{懒汉式单例下面的 - 双重检测锁DCL方式实现单例模式}
3. 彻底玩转单例模式
双重检测锁/双重检验锁/双重检查锁(Double Checked Locking) DCL模式的懒汉式单例{线程安全}
package com.gch.dcl;/**DCL双重检验锁机制在多线程环境下实现延迟加载和线程安全的懒汉式单例模式{懒加载}*/
public class SingleInstance {/**1.构造器私有*/private SingleInstance() {}/**2.定义一个私有的用volatile修饰的静态成员变量存储一个对象 => 保证内存可见性防止JVM指令重排*/private volatile static SingleInstance instance;/**3.提供一个方法,对外返回单例对象*/public static SingleInstance getInstance(){/** 双重检验锁的逻辑式通过两次检查instance变量是否为null来实现延迟加载 */// 这是为了防止多个线程同时通过第一次检查,然后同时进入代码块创建实例的情况。// 先判断对象是否已经被实例过{第一次检查}if(instance == null){// 对象没有被实例化过,进入加锁代码{类对象加锁}synchronized(SingleInstance.class){// 第二次检查if(instance == null){// 实例化 => 不是一个原子性操作/*** 1.为对象分配内存空空间* 2.执行构造方法,初始化对象* 3.将对象指向分配的内存空间*/instance = new SingleInstance();}}}return instance;}
}
- 由于JVM具有指令重排的特性,执行顺序有可能变成1 -> 3 -> 2。
- 指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。
- 例如:线程T1执行了1和3,此时线程T2调用了getInstance()方法后方法instance不为空,因此直接返回instance,但此时instance变量还未被初始化。
- 因此instance变量使用volatile关键字修饰很有必要,可以禁止JVM的指令重排,保证在多线程环境下也能正常运行。
通过反射暴力破解单例模式:
package com.gch.dcl;import java.lang.reflect.Constructor;public class Main {public static void main(String[] args) throws Exception {// 1.获取Class字节码文件的对象Class<SingleInstance> clazz = SingleInstance.class;// 2.获取到空参的构造方法Constructor<SingleInstance> constructor = clazz.getDeclaredConstructor();// 3.暴力反射,表示临时取消访问权限,表示权限被打开constructor.setAccessible(true);// 4.利用获取到的构造方法去创建对象SingleInstance instance1 = constructor.newInstance();SingleInstance instance2 = constructor.newInstance();System.out.println(instance1);System.out.println(instance2);System.out.println(instance1 == instance2);}
}
使用枚举实现单例 - 通过枚举防止反射破解创建实例对象:
不能使用反射破坏枚举
枚举类的实例是在类加载时被初始化的,而且枚举类不支持反射{创建实例},因此使用枚举类实现的单例模式是天然防止反射破解的。
使用枚举实现单例模式的优点:
- 线程安全:枚举的实例在任何情况下都是唯一的,不需要担心多线程环境下的竞争条件。
- 防止反射:枚举类不支持反射,可以防止通过反射破解单例模式。
package com.gch.dcl;import java.lang.reflect.Constructor;/**不能使用反射破坏枚举,枚举不支持反射*/
public enum SingleInstanceEnum {INSTANCE;
}class Test{public static void main(String[] args) throws Exception {SingleInstanceEnum instance1 = SingleInstanceEnum.INSTANCE;SingleInstanceEnum instance2 = SingleInstanceEnum.INSTANCE;SingleInstanceEnum instance3 = SingleInstanceEnum.INSTANCE;System.out.println(instance1);System.out.println(instance2);System.out.println(instance3);/**尝试使用反射破坏枚举 => 运行时报错java.lang.NoSuchMethodException*/// 1.获取Class字节码文件的对象Class<SingleInstanceEnum> clazz = SingleInstanceEnum.class;// 2.获取到空参的构造方法Constructor<SingleInstanceEnum> constructor = clazz.getDeclaredConstructor();// 3.暴力反射,表示临时取消访问权限,表示权限被打开constructor.setAccessible(true);// 4.利用获取到的构造方法去创建对象SingleInstanceEnum instance4 = constructor.newInstance();SingleInstanceEnum instance5 = constructor.newInstance();System.out.println(instance4);System.out.println(instance5);}
}
