当前位置: 首页 > ai >正文

【手撕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其底层严格的保证可见性,在进入和退出的时候做了严格的内存同步:

  1. 退出 synchronized 块时(monitorexit 指令)

    • 当前线程的所有修改(包括缓存中的脏数据)必须写回主内存(相当于 volatile 写)。

  2. 进入 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();}
}

http://www.xdnf.cn/news/18488.html

相关文章:

  • C++算法·进制转换
  • DeepSeek V3.1深度解析:一个模型两种思维,迈向Agent时代的第一步!
  • 并查集详解
  • 基于Python的农作物病虫害防治网站 Python+Django+Vue.js
  • 说说你对Integer缓存的理解?
  • 文献阅读笔记【物理信息机器学习】:Physics-informed machine learning
  • 【秋招笔试】2025.08.23美团研发岗秋招笔试题
  • SpringBoot applicationContext.getBeansOfType获取某一接口所有实现类,应用于策略模式
  • 深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)第五章整理
  • 墨刀原型设计工具操作使用指南及实践操作
  • 玩转Vue3高级特性:Teleport、Suspense与自定义渲染
  • 【假设微调1B模型,一个模型参数是16bit,计算需要多少显存?】
  • 【ABAP4】创建Package
  • 【力扣 Hot100】每日一题
  • Agent原理、构建模式(附视频链接)
  • 深度解析Bitmap、RoaringBitmap 的原理和区别
  • 讲点芯片验证中的统计覆盖率
  • 【攻防世界】easyupload
  • 量子计算驱动的Python医疗诊断编程前沿展望(上)
  • WSL Ubuntu数据迁移
  • 【数据分析】宏基因组荟萃分析(Meta-analysis)的应用与实操指南
  • 容器安全实践(三):信任、约定与“安全基线”镜像库
  • 应用篇#1:YOLOv8模型在Windows电脑摄像头上的部署
  • 26.内置构造函数
  • c# .net支持 NativeAOT 或 Trimming 的库是什么原理
  • 数据库优化提速(三)JSON数据类型在酒店管理系统搜索—仙盟创梦IDE
  • python企微发私信
  • 【React ✨】从零搭建 React 项目:脚手架与工程化实战(2025 版)
  • 文字学的多维透视:从符号系统到文化实践
  • 2025年09月计算机二级MySQL选择题每日一练——第五期