Java核心知识体系7:线程安全性讨论
Java核心知识体系1:泛型机制详解
Java核心知识体系2:注解机制详解
Java核心知识体系3:异常机制详解
Java核心知识体系4:AOP原理和切面应用
Java核心知识体系5:反射机制详解
Java核心知识体系6:集合框架详解
1 为什么需要多线程
我们都知道,CPU、内存、I/O 设备的速度是有极大差异的,为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系结构、操作系统、编译程序都做出了优化,主要体现为:
- CPU增加了缓存,均衡了与内存之间的速度差异,但会导致可见性问题
- 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异,但会导致原子性问题
- 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用,但会导致有序性问题
从上面可以看到,虽然多线程平衡了CPU、内存、I/O 设备之间的效率,但是同样也带来了一些问题。
2 线程不安全案例分析
如果有多个线程,对一个共享数据进行操作,但没有采取同步的话,那操作结果可能超出预想,产生不一致。
下面举个粒子,设置一个计数器count,我们通过1000个线程同时对它进行增量操作,看看操作之后的值,是不是符合预想中的1000。
public class UnsafeThreadTest {
private int count = 0;
public void add() {
count += 1;
}
public int get() {
return count;
}
}
public static void main(String[] args) throws InterruptedException {
final int threadNum = 1000;
UnsafeThreadTest threadTest = new UnsafeThreadTest();
final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
ExecutorService executorSvc = Executors.newCachedThreadPool();
// 执行并发计数
for (int idx = 0; idx < threadNum; idx ++) {
executorSvc.execute(() -> {
threadTest.add();
countDownLatch.countDown();
});
}
countDownLatch.await();
// 关闭线程池
executorSvc.shutdown();
System.out.println("最终计数:" + threadTest.get());
}
最终计数:994 // 结果跟预期的 1000 不一样
3 并发出现问题的原因
可以看到,上述代码输出的结果跟预期的 1000 不一样,我们需要理清楚发生了什么问题?
★ 并发三要素:可见性、原子性、有序性
3.1 可见性:由CPU缓存引起
CPU缓存是一种高速缓存,用于存储CPU最近使用的数据。由于CPU缓存比主存储器更快,因此CPU会尽可能地使用缓存,以提高程序的性能。但是,这也会导致可见性问题。
可见性问题是指当一个线程修改了一个共享变量的值时,另一个线程可能无法立即看到这个修改。
我们举个简单的例子,看下面这段代码:
// 主存中 index 的值默认为 10
System.out.println("主存中的值:" + index);
// Thread1 执行赋值
index = 100;
// Thread2 执行的
threadA = index;
因为Thread1修改后的值可能仍然存储在CPU缓存中,而没有被写回主存储器。这种情况下,Thread2无法读取到修改后的值,所以导致错误信息。
具体来说,当多个线程同时运行在同一个处理器上时,它们共享该处理器的缓存。如果一个线程修改了某个共享变量的值,该值可能被存储在处理器缓存中,并且未被立即写回到主存储器中。
因此,当另一个线程试图读取该变量的值时,它可能会从主存储器中读取旧的值 10,而不是从处理器缓存中读取已更新的值 100。
3.2 原子性: 由分时复用引起
原子性:原子性是指一个操作在执行过程中不可分割,即该操作要么完全执行,要么完全不执行。
我们举个简单的例子,看下面这段代码:
// 主存中 index 的值默认为 10
System.out.println("主存中的值:" + index);
// Thread1 执行增值
index += 1;
// Thread2 执行增值
index += 1
以上的信息可以看出:
- 主存的值为10
- i += 1 这个操作实际执行三条 CPU 指令
- 变量 i 从内存读取到 CPU寄存器;
- 在CPU寄存器中执行 i + 1 操作;
- 将最后的结果i写入内存,因为有缓存机制,所以最终可能写入的是 CPU 缓存而不是内存。
- 由于CPU分时复用(线程切换)的存在,Thread1执行了第一条指令后,就切换到Thread2执行,Thread2全部执行完成之后,再切换会Thread1执行后续两条指令,将造成最后写到内存中的index值是11而不是12。
3.3 有序性: 重排序引起
有序性:即程序执行的顺序按照代码的先后顺序执行。
重排序(Reordering)是指在计算机系统中,由于处理器优化或编译器优化等原因,导致指令执行的顺序与程序代码中的顺序不一致。重排序可能会引起有序性错误,即在并发或多线程环境中,程序执行的顺序与代码的先后顺序不一致,导致程序结果不正确或出现意外的结果。
我们举个简单的例子,看下面这段代码:
int idx = 10;
boolean isCheck = true;
idx += 1; // 执行语句1
isCheck = false; // 执行语句2
上面代码定义了一个int型变量,定义了一个boolean类型变量,然后分别对两个变量进行操作。
从代码顺序上看,执行语句1是在执行语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗? 不一定,为什么呢? 这里可能会发生指令重排序(Instruction Reorder)。
重排序(Reordering)是指在计算机系统中,由于处理器优化或编译器优化等原因,导致指令执行的顺序与程序代码中的顺序不一致。重排序可能会引起有序性错误,即在并发或多线程环境中,程序执行的顺序与代码的先后顺序不一致,导致程序结果不正确或出现意外的结果。
重排序引起的有序性错误主要有以下几种情况:
- 指令重排序:处理器为了优化程序的执行,可能会对指令进行重排序。这种重排序不会改变单线程程序的执行结果,但可能会影响多线程程序的行为。例如,一个线程修改了一个共享变量的值,但由于指令重排序,另一个线程在读取该变量时可能读取到过时的值。
- 内存访问重排序:处理器为了提高程序的执行效率,可能会对内存访问进行重排序。例如,一个线程先读取一个共享变量的值,然后再写入该值,但由于内存访问重排序,处理器可能会先执行写入操作,再执行读取操作,从而导致其他线程无法正确地读取到修改后的值。
- 同步操作重排序:在并发或多线程环境中,同步操作可能会被重排序。例如,一个线程先释放了一个锁,然后再执行另一个操作,但由于同步操作重排序,释放锁的操作可能会先于另一个操作执行,从而导致其他线程无法正确地获取锁。

为了避免重排序引起的有序性错误,可以采用一些同步机制来确保程序的执行顺序,如内存屏障(Memory barrier,intel 称为 memory fence)、指令fence等。这些同步机制可以确保指令的执行顺序与代码中的顺序一致,避免指令重排序和内存访问重排序等问题。同时,也可以使用串行化(Serialization)或事务内存(Transactional memory)等技术来保证并发程序的有序性。
4 总结
- CPU、内存、I/O 设备的速度是有极大差异的,多线程 的实现是为了合理利用 CPU 的高性能,平衡这三者的速度差异
- 多线程情况下,并发产生问题的三要素:可见性、原子性、有序性
- 可见性:由CPU缓存引起
- 原子性: 由分时复用引起
- 有序性: 重排序引起
Java核心知识体系7:线程安全性讨论的更多相关文章
- Java核心知识体系3:异常机制详解
1 什么是异常 异常是指程序在运行过程中发生的,由于外部问题导致的运行异常事件,如:文件找不到.网络连接失败.空指针.非法参数等. 异常是一个事件,它发生在程序运行期间,且中断程序的运行. Java ...
- Java核心知识体系4:AOP原理和切面应用
1 概述 我们所说的Aop(即面向切面编程),即面向接口,也面向方法,在基于IOC的基础上实现. Aop最大的特点是对指定的方法进行拦截并增强,这种增强的方式不需要业务代码进行调整,无需侵入到业务代码 ...
- Java核心知识体系2:注解机制详解
1 Java注解基础 注解是JDK1.5版本开始引入的一个特性,用于对程序代码的说明,可以对包.类.接口.字段.方法参数.局部变量等进行注解. 它主要的作用有以下四方面: 生成javadoc文档,通过 ...
- Java并发编程核心知识体系精讲
第1章 开宗明义[不看错过一个亿]本章一连串设问:为什么学并发编程?学并发编程痛点?谁适合学习本课?本课程包含内容和亮点?首先4大个理由告诉你为什么要学,其实源于JD岗位要求就不得不服了.其次5个痛点 ...
- Python 编程核心知识体系(REF)
Python 编程核心知识体系: https://woaielf.github.io/2017/06/13/python3-all/ https://woaielf.github.io/page2/
- 最强最全的Java后端知识体系
目录 最全的Java后端知识体系 Java基础 算法和数据结构 Spring相关 数据库相关 方法论 工具清单 文档 @(最强最全的Java后端知识体系) 最全的Java后端知识体系 最全的Java后 ...
- JVM核心知识体系(转http://www.cnblogs.com/wxdlut/p/10670871.html)
1.问题 1.如何理解类文件结构布局? 2.如何应用类加载器的工作原理进行将应用辗转腾挪? 3.热部署与热替换有何区别,如何隔离类冲突? 4.JVM如何管理内存,有何内存淘汰机制? 5.JVM执行引擎 ...
- 学习Java的知识体系路线(详细完整版,附图加目录)
将网上的Java学习路线图进行归纳囊括,方便以后学习时弥补自身所欠缺的知识点,也算是给自己一个明确的学习方向.至于第一阶段,即JavaSE的基础,这里不给出. 第二阶段 技术名称 技术内容 数据库技术 ...
- Java学习知识体系大纲梳理
感悟 很奇怪,我怎么会想着写这么一篇博客——Java语言的学习体系,这不是大学就已经学过的课程嘛.博主系计算机科班毕业,大学的时候没少捧着Java教程来学习,不管是为了学习编程还是为了期末考个高分,都 ...
- Java 多线程:什么是线程安全性
线程安全性 什么是线程安全性 <Java Concurrency In Practice>一书的作者 Brian Goetz 是这样描述"线程安全"的:"当多 ...
随机推荐
- K8S | Config应用配置
绕不开的Config配置: 一.背景 在自动化流程中,对于一个应用来说,从开发阶段的配置管理,到制作容器镜像,再到最后通过K8S集群发布为服务,整个过程涉及到的配置非常多: 应用环境:通常是指代码层面 ...
- [mysql]MGR简介与部署
前言 MySQL Group Replication,简称MGR,是MySQL官方于2016年推出的一个全新的高可用解决方案,采用Paxos分布式一致性协议作为高可用和一致性解决方案.在MGR之前的高 ...
- 论文解读(TAMEPT)《A Two-Stage Framework with Self-Supervised Distillation For Cross-Domain Text Classification》
论文信息 论文标题:A Two-Stage Framework with Self-Supervised Distillation For Cross-Domain Text Classificati ...
- 小白整理了VUEX
在小白开发的项目中前端使用的是Vue,虽然在日常工作中可以使用Vue进行开发工作.但是没有系统的学习过Vue,对Vue的一些特性和方法使用时常常需要查询资料解决问题,查询资料常常会占用大量时间,尤其对 ...
- redis分布式锁,setnx+lua脚本的java实现
1 前言 在现在工作中,为保障服务的高可用,应对单点故障.负载量过大等单机部署带来的问题,生产环境常用多机部署.为解决多机房部署导致的数据不一致问题,我们常会选择用分布式锁. 目前其他比较常见的实现方 ...
- API接口获取快手商品详情(封装代码)
快手是中国最大的短视频平台之一,也是许多电商企业进行推广的重要渠道.为了更好地了解快手的商品信息,我们可以通过API接口来获取商品详情. 首先,我们需要了解快手API接口和相应的文档 接下来,我们需要 ...
- API数据接口获取商品详情数据该怎么操作
随着电商行业的快速发展,获取商品详情数据是电商应用的重要功能之一.为了提高用户的购物体验,开发者可以利用API接口获取商品详情数据,方便用户了解商品的具体信息和特点.在本文中,我们将介绍如何使用API ...
- TIKZ全局样式设置
tikz绘图引擎 TIKZ绘图引擎是基于tex实现,代码极其复杂,每次编写都要单独设置样式,甚是繁琐,如何能够全局设置呢? \begin{tikzpicture}[ auto, % 决策结点 deci ...
- Field 'xxxxxx' doesn't have a default value 错误的解决办法
在写web项目的时候,出现 Field 'xxxxx' doesn't have a default value 这个错误,直接找到你的数据库,然后打开设计表,看下面自增递增是否勾选上了,如果没有勾上 ...
- Solution -「洛谷 P5046」「YunoOI 2019 模拟赛」Yuno loves sqrt technology I
Description Link. 无修改区间求逆序对. Solution 首先有一个显然的 \(\Theta(N\sqrt{N}\log_{2}N)\) 做法,由于过不了所以我就不废话. 其实有了 ...