一个既简单又诡异的问题

public class DaYaoGuai {static String s;public static void main(String[] args) {Thread t1 = new Thread(){@Overridepublic void run() {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}s="深圳";}};t1.start();Thread t2 = new Thread(){@Overridepublic void run() {while (s==null){System.out.println("空的");//屏蔽掉这一句,就永远都不会打印出"深圳";有这一句就能打印出"深圳"}System.out.println(s);}};t2.start();}
}

上面的这句 System.out.println("空的") 到底影响了什么?怎么会对结果有那么大影响?

2025年4月21日补充

先说结论:与内存模型、指令优化有关,没有System.out.println("空的")的话,该线程使用的将会一直是它工作内存里缓存的s,而System.out.println("空的")能触发同步进而刷新s。

这一代码存在线程安全方面的问题,因为静态变量s未被volatile关键字修饰。在 Java 里,每个线程都有自己的工作内存,线程操作变量时,先把变量从主内存复制到工作内存,操作完成之后再写回主内存。线程t1修改了s的值,不过这个修改在主内存里,线程t2工作内存中的s副本可能仍然是null。要是线程t2一直使用工作内存中的s副本,那么while (s == null)这个循环条件始终为真,循环不会结束,也就无法打印出 “深圳”。svolatile关键字修饰后,线程t1s的修改会马上刷新到主内存,线程t2也会立即从主内存读取s的最新值,这样就能保证线程t2能正确打印出 “深圳”。

那么,为什么t1修改了s的值后,t2工作内存中的副本不能及时更新?

现代处理器为了提高性能,会采用缓存、指令重排序等优化手段。

  • 缓存机制:处理器会将经常访问的数据存储在高速缓存中,以减少对主内存的访问次数。每个处理器核心都有自己的缓存,当一个线程在某个处理器核心上运行时,它对变量的操作是在该核心的缓存中进行的。当t1线程修改了s的值,这个修改可能只存在于t1所在处理器核心的缓存中,而t2所在处理器核心的缓存中的s值仍然是旧的。
  • 指令重排序:为了提高程序的执行效率,编译器和处理器可能会对指令进行重排序。在t2线程中,编译器或处理器可能会对while (s == null)这个循环进行优化,将s的读取操作缓存起来,不再每次都从主内存中读取,从而导致t2无法及时获取到s的最新值。

在t2的while循环中加一句System.out.println("空的"),最终也能打印出“深圳",这又是什么原理?

1. 涉及同步

System.out.println 方法是一个同步方法,它的实现内部使用了 synchronized 块。在 Java 中,System.out 是 PrintStream 类的一个实例,println 方法的调用会进入一个同步代码块:

// PrintStream 类中的 println 方法
public void println(String x) {synchronized (this) {print(x);newLine();}
}

根据 Java 内存模型(JMM),进入同步代码块时,线程会从主内存中读取共享变量的最新值,退出同步代码块时,会将工作内存中的数据刷新到主内存。因此,在每次执行System.out.println("空的") 时,线程 t2 都会从主内存中读取 s 的最新值。也就是,synchronized会让线程清空自己的缓存,然后重新去主内存拷贝一份副本,这样,每次System.out.println()执行时其实就是刷新线程变量了。

2. 线程调度和上下文切换

在执行 System.out.println("空的") 时,由于这个操作涉及到 I/O 操作和同步机制,会导致线程 t2 发生上下文切换。上下文切换是指操作系统暂停当前正在执行的线程,保存其状态,然后选择另一个线程执行的过程。

在上下文切换过程中,线程 t2 会释放 CPU 资源,之后再次获得 CPU 资源继续执行时,会从主内存中重新加载共享变量的值。这样,线程 t2 就有机会获取到线程 t1 修改后的 s 的值,从而使 while (s == null) 条件不再成立,退出循环并打印出 “深圳”。

最后一个问题:如此,指令优化、缓存机制等这些为了提高效率的手段岂不是以带来新风险为代价的?

很不幸,确实是这样!坑也挺多的呀!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/80131.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

使用docker在manjaro linux系统上运行windows和ubuntu

因为最近项目必须要使用指定版本的solidworks和maxwell(都只能在win系统上使用), 且目前的ubuntu容器是没有桌面的,导致我运行不了一些带图形的ros2功能。无奈之下,决定使用docker-compose写一下配置文件,彻底解决问题…

Elasticsearch中的_source字段讲解

_source 在 Elasticsearch 查询中用于限制返回的字段,类似于 SQL 中的 SELECT 指定列。 代码示例: esSearchResults = es_service.search_documents({"query": {"terms": {"file_id":

【论文阅读20】-CNN-Attention-BiGRU-滑坡预测(2025-03)

这篇论文主要探讨了基于深度学习的滑坡位移预测模型,结合了MT-InSAR(多时相合成孔径雷达干涉测量)观测数据,提出了一种具有可解释性的滑坡位移预测方法。 [1] Zhou C, Ye M, Xia Z, et al. An interpretable attention-based deep…

C++ 的 IO 流

💬 :如果你在阅读过程中有任何疑问或想要进一步探讨的内容,欢迎在评论区畅所欲言!我们一起学习、共同成长~! 👍 :如果你觉得这篇文章还不错,不妨顺手点个赞、加入收藏,并…

spring cloud gateway前面是否必须要有个nginx

在 **"客户端 → Nginx (前置限流) → Spring Cloud Gateway → 微服务(Sentinel 熔断限流)"** 的架构中,**Spring Cloud Gateway 前面并不强制要求必须有 Nginx**,是否需要取决于具体场景。以下是详细分析: 一、必须使用 Nginx 的…

Spark和Hadoop之间的对比和联系

(一)Spark概述 Spark是一种基于内存的快速、通用、可拓展的大数据分析计算引擎。Hadoop是一个分布式系统基础架构。 1)官网地址:Apache Spark™ - Unified Engine for large-scale data analytics 2)文档查看地址&…

多线程进阶(Java)

注:此博文为本人学习过程中的笔记 1.常见的锁策略 当我们需要自己实现一把锁时,需要关注锁策略。Java提供的synchronized已经非常好用了,覆盖了绝大多数的使用场景。此处的锁策略并不是和Java强相关的,只要涉及到并发编程&#…

c++STL——stack、queue、priority_queue的模拟实现

文章目录 stack、queue、priority_queue的模拟实现使用部分模拟实现容器适配器deque的介绍原理真实结构deque的迭代器deque的操作deque的优缺点 stack的模拟实现按需实例化queue的模拟实现priority_queue的模拟实现为何引入仿函数代码实现 stack、queue、priority_queue的模拟实…

【深度学习—李宏毅教程笔记】Transformer

目录 一、序列到序列(Seq2Seq)模型 1、Seq2Seq基本原理 2、Seq2Seq模型的应用 3、Seq2Seq模型还能做什么? 二、Encoder 三、Decoder 1、Decoder 的输入与输出 2、Decoder 的结构 3、Non-autoregressive Decoder 四、Encoder 和 De…

C++镌刻数据密码的树之铭文:二叉搜索树

文章目录 1.二叉搜索树的概念2.二叉搜索树的实现2.1 二叉搜索树的结构2.2 二叉搜索树的节点寻找2.2.1 非递归2.2.2 递归 2.3 二叉搜索树的插入2.3.1 非递归2.3.2 递归 2.4 二叉搜索树的删除2.4.1 非递归2.4.2 递归 2.5 二叉搜索树的拷贝 3.二叉树的应用希望读者们多多三连支持小…

系统架构设计师:流水线技术相关知识点、记忆卡片、多同类型练习题、答案与解析

流水线记忆要点‌ ‌公式 总时间 (n k - 1)Δt 吞吐率 TP n / 总时间 → 1/Δt(max) 加速比 S nk / (n k - 1) | 效率 E n / (n k - 1) 关键概念 周期:最长段Δt 冲突‌: ‌数据冲突(RAW) → 旁路/…

强制重装及验证onnxruntime-gpu是否正确工作

#工作记录 我们经常会遇到明明安装了onnxruntime-gpu或onnxruntime后,无法正常使用的情况。 一、强制重新安装 onnxruntime-gpu 及其依赖 # 强制重新安装 onnxruntime-gpu 及其依赖 pip install --force-reinstall --no-cache-dir onnxruntime-gpu1.18.0 --extra…

桌面我的电脑图标不见了怎么恢复 恢复方法指南

在Windows操作系统中,“我的电脑”或在较新版本中称为“此电脑”的图标,是访问硬盘驱动器、外部存储设备和系统文件的重要入口。然而,有些用户可能会发现桌面上缺少了这个图标,这可能是由于误操作、系统设置更改或是不小心删除造成…

2025.04.20【Lollipop】| Lollipop图绘制命令简介

Customize markers See the different options allowing to customize the marker on top of the stem. Customize stems See the different options allowing to customize the stems. 文章目录 Customize markersCustomize stems Lollipop图简介R语言中的Lollipop图使用ggp…

docker-compose搭建kafka

1、单节点docker-compose.yml version: 3 services:zookeeper:image: zookeeper:3.8container_name: zookeeperports:- "2181:2181"volumes:- ./data/zookeeper:/dataenvironment:ZOO_MY_ID: 1ZOO_MAX_CLIENT_CNXNS: 100kafka:image: bitnami/kafka:3.7container_na…

【问题】一招解决vscode输出和终端不一致的困扰

背景(闲话Trae) Trae是挺好,用了几天,发现它时不时检查文件,一检测就转悠半天,为此我把当前环境清空,就留一个正在调的程序,结果还照样检测,虽然没影响什么,…

Git,本地上传项目到github

一、Git的安装和下载 https://git-scm.com/ 进入官网,选择合适的版本下载 二、Github仓库创建 点击右上角New新建一个即可 三、本地项目上传 1、进入 要上传的项目目录,右键,选择Git Bash Here,进入终端Git 2、初始化临时仓库…

从零开始配置spark-local模式

1. 环境准备 操作系统:推荐使用 Linux 或 macOS,Windows 也可以,但可能会有一些额外的配置问题。 Java 环境:Spark 需要 Java 环境。确保安装了 JDK 1.8 或更高版本。 检查 Java 版本: bash 复制 java -version 如果…

前端~地图(openlayers)绘制车辆运动轨迹(仿高德)

绘制轨迹路线轨迹路线描边增加起点终点图标绘制仿高德方向箭头模仿车辆动态运动动画 车辆运行轨迹 车辆轨迹经纬度坐标 const linePoints [new Point([123.676031, 43.653421]),new Point([123.824347, 43.697124]),new Point([124.197882, 43.946811]),new Point([124.104498…

分布式之CAP原则:理解分布式系统的核心设计哲学

声明:CAP中的P原则都是需要带着的 在分布式系统的设计与实践中,CAP原则(又称CAP定理)是开发者必须掌握的核心理论之一。它揭示了分布式系统在一致性(Consistency)、可用性(Availability&#x…