【手撕JAVA多线程:2.线程安全】 2.1.JVM层面的线程安全保证
目录
概述
happen-before和as-if-serial特性
Synchronized
实现
代码示例
volatile
实现
代码示例
概述
本文其实就是讲JMM相关内容,但是由于是想精炼JAVA多线程相关内容,所以不会铺开讲细节,细节前面有文章讲过:
【JAVA多线程】JMM,成体系聊一下JAVA线程安全问题_从jmm解释线程安全问题-CSDN博客
本文,包括本系列是想将整个JAVA多线程的内容精炼出来,以帮助大家形成成体系且精炼的一个认知,所以讲究只讲绝对的精华,要展开之前都有对应的文章。好的开始!
再重复一遍,JAVA多线程的核心内容:
-
线程操作
-
线程安全
-
线程编排
本文将聊一下线程安全部分,要聊JAVA的线程安全,要先知道JAVA线程安全问题的:
-
不可见性,一条线程对数据做了修改还没从cache中刷回内存中,另外的线程就去读取了数据,那么前一条线程做的数据修改对后面去读取数据的线程来说就是不可见的,从而造成了数据的不同步。
-
指令重排序,操作系统为了保证程序执行的高效,有时候会对程序中的指令进行重排序,这种重排序可能会造成多线程间执行结果不同,造成数据不一致的线程安全问题。
JAVA提供了两个层面的线程安全的保证:
-
JVM层的保证:Synchronized、volatile
-
留给开发者的更灵活控制:Lock,Lock底层其实就是用的volatile+CAS,相当于是JDK封装了一个线程安全的工具类给开发者用,免得开发者从0开始造轮子罢了。
happen-before和as-if-serial特性
前面聊了多线程环境下,造成线程安全问题的两大原因是不可见性和指令重排序。我们知道从逻辑上来说要实现一些核心诉求,就要保证实现一些特性,比如数据库为了实现事务,就要保证呈现出ACID的特性。保证多线程环境下的线程安全也是,只要实现as-if-serial、happen-before两个特性即可。
-
happen-before,用来保证可见性,A happen-before B, 则A的执行结果必须对B可见。由Synchronized关键字来保证。
-
as-if-serial,用来保证可见性和指令不被重排序,由volatile关键字来保证。
特别注意:Synchronized和volatile虽说都可以保证可见性,但是两者是没办法相互替代的,Synchronized是用来保证同一时间只有单一线程持有资源的,volatile是用来保证volatile修饰的变量的读写操作前后的那些操作不会被重排序,用来保证操作的有序性。
Synchronized
实现
Synchronized用来修饰方法、变量、一块代码块。修饰方法或者一块代码块的时候用来制造出一块“临界区”(操作系统概论中的概念,即同一时间只允许一条线程进入的区域),修饰一个变量的时候,用来制造出一个临界资源(操作系统概论中的概念,即同一时间只允许一条线程持有的资源)
Synchronized是利用对象的Mark Word来实现的,如果Synchronized修饰的是变量用的就是变量的对象的对象头里面的Mark Word,如果Synchronized用来制造一个同步块利用的就是被持有的对象的对象头的Mark Word,如果Synchronized修饰的是方法,利用的就是this对象的对象头的Mark Word。
具体的实现以及经典的锁升级过程
,看上一篇文章:
【手撕JAVA多线程】1.从设计初衷去看JAVA的线程操作-CSDN博客
这里唯一要拿出来单独说的是synchronized并不是当持有资源的线程执行完就唤醒其他线程去立马争抢资源,如果是持有资源的线程执行完就唤醒其他线程去立马争抢资源,还是会存在数据没有回写的可能性,synchronized其底层严格的保证可见性,在进入和退出的时候做了严格的内存同步:
-
退出
synchronized
块时(monitorexit
指令):-
当前线程的所有修改(包括缓存中的脏数据)必须写回主内存(相当于
volatile
写)。
-
-
进入
synchronized
块时(monitorenter
指令):-
线程必须从主内存重新加载变量(相当于
volatile
读)。
-
代码示例
Synchronized和volatile虽说都可以保证可见性,但是两者是没办法相互替代的,Synchronized是用来保证同一时间只有单一线程持有资源的,volatile是用来保证volatile修饰的变量的读写操作前后的那些操作不会被重排序,用来保证操作的有序性。
场景:多个线程同时修改同一个计数器,保证最终结果正确。
public class SynchronizedExample {private int count = 0;
// synchronized 方法,保证原子性和可见性public synchronized void increment() {count++;}
public static void main(String[] args) throws InterruptedException {SynchronizedExample example = new SynchronizedExample();
// 创建两个线程,每个线程对 count 累加 1000 次Thread t1 = new Thread(() -> {for (int i = 0; i < 1000; i++) {example.increment();}});
Thread t2 = new Thread(() -> {for (int i = 0; i < 1000; i++) {example.increment();}});
t1.start();t2.start();
t1.join(); // 等待 t1 完成t2.join(); // 等待 t2 完成
System.out.println("Final count: " + example.count); // 正确输出 2000}
}
volatile
实现
volatile,JAVA虚拟机提供的最轻量级的同步机制。其通过实现“缓存一致性协议”和“内存屏障”,保证了happen-before和强制禁止了指令重排序。
-
缓存一致性协议 保证工作内存(缓存)中的数据和主内存(内存)中的数据的一致性,即一旦工作内存中的数据有变,马上刷新回主内存。 其底层实现是CPU的嗅探机制,所有CPU都盯住总线,监听总线中的数据变化,一旦工作内存中存 在的数据在总线中出现了assign操作,会立即让工作内存中的相应值失效,从而重新从主内存中去读取值。
-
不同的CPU有不同的缓存一致性协议。 内存屏障用于禁止指令重排序, 具体的实现是在需要禁止重排序的两条代码(指令)之间插入一个标志,标 识标志两边的代码(指令)禁止重排序。这个标志是汇编级别的。
代码示例
Synchronized和volatile虽说都可以保证可见性,但是两者是没办法相互替代的,Synchronized是用来保证同一时间只有单一线程持有资源的,volatile是用来保证volatile修饰的变量的读写操作前后的那些操作不会被重排序,用来保证操作的有序性,是无法保证同一时间只有单一线程持有资源的。
场景:一个线程修改标志位,另一个线程读取标志位并退出循环。
volatile用来保证在flag = true;
前后的指令不会被重排序
public class VolatileExample {private volatile boolean flag = false; // 使用 volatile 保证可见性
public void start() {// 线程1:1秒后修改 flagnew Thread(() -> {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}flag = true; // 修改 flagSystem.out.println("Flag set to true");}).start();
// 线程2:循环检测 flag,直到 flag=true 才退出new Thread(() -> {while (!flag) {// 空循环,等待 flag 变化}System.out.println("Flag detected as true, exiting...");}).start();}
public static void main(String[] args) {VolatileExample example = new VolatileExample();example.start();}
}