Java并发编程的本质是解决这三大问题
【本文版权归微信公众号"代码艺术"(ID:onblog)所有,若是转载请务必保留本段原创声明,违者必究。若是文章有不足之处,欢迎关注微信公众号私信与我进行交流!】
前言
并发编程的本质其实是要解决:可见性、原子性、有序性这三大问题。
相信这句话你已经听了无数遍,那我问你,单核CPU是否有并发问题,是否还需要加锁呢?线程的工作内存在哪里,你可别给我说是栈。
本文就是想搞清楚一直的聊的并发编程问题,究竟是什么?
可见性
学过计算机组成原理的我们知道,计算机存储系统的层次结构主要体现在缓存 - 主存和主存 - 辅存这两个存储层次上,如图所示。显然,CPU 和缓存、主存都能直接交换信息;缓存能直接和CPU、主存交换信息;主存可以和CPU、缓存、辅存交换信息。
(注:主存指 RAM 和 ROM;辅存指光盘、磁带、磁盘等)
对于如今的多核处理器,CPU的每个内核都有自己的缓存,而缓存仅仅对它所在的处理器可见,所以缓存向主存刷新数据时就容易造成数据的不一致问题。如图所示。
在Java内存模型中提到了线程栈为线程的工作内存,其实线程的工作内存是对 CPU 寄存器和高速缓存的抽象描述,使用频率高的数据从主存拷贝到高速缓存中,每个线程在 CPU 高速缓存中对拷贝的数据进行读取、计算、赋值,再在合适的时候同步更新到主存的该数据。
所谓的可见性,就是一个线程对共享变量的修改,另外一个线程能够立刻看到。
导致可见性问题的原因就是缓存不能及时刷新至主存。
例如一段代码如下所示:
public class PrintString implements Runnable{
private boolean isContinuePrint = true;
@Override
public void run() {
while (isContinuePrint){
System.out.println("Thread: "+Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public boolean isContinuePrint() {
return isContinuePrint;
}
public void setContinuePrint(boolean continuePrint) {
isContinuePrint = continuePrint;
}
public static void main(String[] args) throws InterruptedException {
PrintString printString = new PrintString();
Thread thread = new Thread(printString,"Thread-A");
thread.start();
Thread.sleep(100);
System.out.println("我要停止它!" + Thread.currentThread().getName());
printString.setContinuePrint(false);
}
}
JVM有Client和Server两种模式,我们可以通过运行:java -version
来查看 JVM 默认工作在什么模式。我们在IDE中把 JVM 设置为在 Server 服务器的环境中,具体操作只需配置运行参数为 -server
。然后启动程序,打印结果:
Thread begin: Thread-A
我要停止它!main
代码 System.out.println("Thread end: "+Thread.currentThread().getName());
从未被执行。
是什么样的原因造成将JVM设置为 -server 就出现死循环呢?
在启动thread线程时,变量boolean isContinuePrint = true;
存在于公共堆栈及线程的私有堆栈中。在JVM设置为 -server 模式时为了线程运行的效率,线程一直在私有堆栈中取得 isRunning
的值是 true。而代码 thread.setRunning(false);
虽然被执行,更新的却是公共堆栈中的 isRunning
变量值 false,所以一直就是死循环的状态。内存结构图:
这个问题其实就是线程工作内存中的值和主内存中的值不同步造成的。解决这样的问题就要使用volatile
关键字了,它主要的作用就是当线程访问 isRunning
这个变量时,强制性从主内存中进行取值。内存结构图:
此图是不是和本文最开始所讲,CPU 可与主存直接进行信息交换一致呢,果然,JVM 内存模型只是对计算机系统的一层封装。
原子性
原子性是什么?
把一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性。
为什么会有原子性问题?
线程是CPU调度的基本单位。CPU会根据不同的调度算法进行线程调度,将时间片分派给线程。当一个线程获得时间片之后开始执行,在时间片耗尽之后,就会失去CPU使用权。多线程场景下,由于时间片在线程间轮换,就会发生原子性问题。
如:对于一段代码,一个线程还没执行完这段代码但是时间片耗尽,在等待CPU分配时间片,此时其他线程可以获取执行这段代码的时间片来执行这段代码,导致多个线程同时执行同一段代码,也就是原子性问题。
线程切换带来原子性问题。
在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。
i = 0; // 原子性操作
j = i; // 不是原子性操作,包含了两个操作:读取i,将i值写回给j
i++; // 不是原子性操作,包含了三个操作:读取i值、i + 1 、将结果写回给i
i = j + 1;// 不是原子性操作,包含了三个操作:读取j值、j + 1 、将结果写回给i
自增操作实际是 3 个离散操作的简写形式:获取当前值,加 1,写回新值。这是一个“读-改-写”操作的实例。
要想保证自增操作的原子性,可以在自增操作中使用 synchronized
关键字进行加锁,代码如下:
public class ThreadTest extends Thread {
int i = 0;
@Override
public void run(){
for (int j = 0; j < 1000; j++) {
synchronized (ThreadTest.class) {
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
ThreadTest threadTest = new ThreadTest();
for (int i = 0; i < 10; i++) {
new Thread(threadTest).start();
}
Thread.sleep(2000);
System.out.println(threadTest.i);
}
}
这段程序的作用是将 int 变量 i 通过 10 个线程累加到 10000,运行后可以看到程序的结果符合我们的预期,原因分析如下:
上面我们说了线程拥有自己的工作内存(寄存器或缓存),但是上图中只标识出写入内存,因为 synchronized
不止可以保证我们“读-改-写”操作的原子性,还可以保证内存的可见性,即由 CPU 直接对主存进行信息交换。
有序性
【本文版权归微信公众号"代码艺术"(ID:onblog)所有,若是转载请务必保留本段原创声明,违者必究。若是文章有不足之处,欢迎关注微信公众号私信与我进行交流!】
有序性:程序执行的顺序按照代码的先后顺序执行。
编译器为了优化性能,有时候会改变程序中语句的先后顺序。例如程序中:a=6;b=7;
编译器优化后可能变成b=7;a=6;
,在这个例子中,编译器调整了语句的顺序,但是不影响程序的最终结果。不过有时候编译器及解释器的优化可能导致意想不到的效果。
有序性问题举例
Java中的一个经典的案例:利用双重检查锁创建单例对象。
public class Singleton {
static Singleton instance;
static Singleton getInstance(){
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
在获取实例 getInstance() 的方法中,我们首先判断 instance 是否为空,如果为空,则锁定 Singleton.class 并再次检查 instance 是否为空,如果还为空则创建 Singleton 的一个实例。
看似很完美,既保证了线程完全的初始化单例,又经过判断 instance 为 null 时再用synchronized 同步加锁。但是还有问题!
instance = new Singleton();
创建对象的代码,分为三步:
①分配内存空间
②初始化对象Singleton
③将内存空间的地址赋值给 instance
但是这三步经过重排之后:
①分配内存空间
②将内存空间的地址赋值给instance
③初始化对象Singleton
会导致什么结果呢?
线程 A 先执行 getInstance() 方法,当执行完指令 ② 时恰好发生了线程切换,切换到了线程 B 上;如果此时线程 B 也执行 getInstance() 方法,那么线程 B 在执行第一个判断时会发现 instance!=null
,所以直接返回 instance,而此时的 instance 是没有初始化过的,如果我们这个时候访问 instance 的成员变量就可能触发空指针异常。
时序图如下:
解决这个问题的方法就是使用 volatile
关键字。对修饰变量的操作不会与其他的内存操作一起重排序,即其具有禁止指令重排序的功能。
问题
回到最初的问题,单核CPU是否有并发问题,是否还需要加锁呢?
学习到这里,相信你已经明白了,单核CPU只能说具有天然的内存可见性,但并发问题涉及的原子性和有序性,依旧还是需要自行解决。
声明
本文所述观点如有不足请留言告知,多谢。
参考资料
https://mp.weixin.qq.com/s/rkl916p8RIErGn58DNcihw
版权声明
【本文版权归微信公众号"代码艺术"(ID:onblog)所有,若是转载请务必保留本段原创声明,违者必究。若是文章有不足之处,欢迎关注微信公众号私信与我进行交流!】
Java并发编程的本质是解决这三大问题的更多相关文章
- Java并发编程实战 02Java如何解决可见性和有序性问题
摘要 在上一篇文章当中,讲到了CPU缓存导致可见性.线程切换导致了原子性.编译优化导致了有序性问题.那么这篇文章就先解决其中的可见性和有序性问题,引出了今天的主角:Java内存模型(面试并发的时候会经 ...
- Java并发编程——线程安全及解决机制简介
简介: 本文主要介绍了Java多线程环境下,可能会出现的问题(线程不安全)以及相应的解决措施.通过本文,你将学习到如下几块知识: 1. 为什么需要多线程(多线程的优势) 1. 多线程带来的问题—线程安 ...
- Java并发编程实战 03互斥锁 解决原子性问题
文章系列 Java并发编程实战 01并发编程的Bug源头 Java并发编程实战 02Java如何解决可见性和有序性问题 摘要 在上一篇文章02Java如何解决可见性和有序性问题当中,我们解决了可见性和 ...
- Java并发编程实战 04死锁了怎么办?
Java并发编程文章系列 Java并发编程实战 01并发编程的Bug源头 Java并发编程实战 02Java如何解决可见性和有序性问题 Java并发编程实战 03互斥锁 解决原子性问题 前提 在第三篇 ...
- Java并发编程实战 05等待-通知机制和活跃性问题
Java并发编程系列 Java并发编程实战 01并发编程的Bug源头 Java并发编程实战 02Java如何解决可见性和有序性问题 Java并发编程实战 03互斥锁 解决原子性问题 Java并发编程实 ...
- Java并发编程 | 从进程、线程到并发问题实例解决
计划写几篇文章讲述下Java并发编程,帮助一些初学者成体系的理解并发编程并实际使用,而不只是碎片化的了解一些Synchronized.ReentrantLock等技术点.在讲述的过程中,也想融入一些相 ...
- Java并发编程:Synchronized及其实现原理
Java并发编程系列: Java 并发编程:核心理论 Java并发编程:Synchronized及其实现原理 Java并发编程:Synchronized底层优化(轻量级锁.偏向锁) Java 并发编程 ...
- Java并发编程 Volatile关键字解析
volatile关键字的两层语义 一旦一个共享变量(类的成员变量.类的静态成员变量)被volatile修饰之后,那么就具备了两层语义: 1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了 ...
- Java 并发编程:volatile的使用及其原理
Java并发编程系列: Java 并发编程:核心理论 Java并发编程:Synchronized及其实现原理 Java并发编程:Synchronized底层优化(轻量级锁.偏向锁) Java 并发编程 ...
随机推荐
- [VuePress]个人博客 -- 批处理自动化编译提交 -- 排错记录
建了一个VuePress的个人博客 想着写个批处理,自动编译并上传到GitHub. 结果遇到两个问题, 一个是,vuepress build docs编译后,这个命令执行完就exit了 研究了下bat ...
- [安卓基础] 001.学习Android开发的好教程
如果想自学android,有许多不错的android网站.这里收集了一些,列举如下: 国内 极客学院,这里有非常丰富的视频教程. http://www.jikexueyuan.com/course/a ...
- Python数据分析:pandas玩转Excel(三)
将对象写入Excel工作表. 要将单个对象写入 Excel .xlsx 文件,只需指定目标文件名即可.要写入多个工作表,必须创建具有目标文件名的ExcelWriter对象,并在文件中指定要写入的工作表 ...
- 设计MyTime类 代码参考
#include <iostream> #include <cstdio> using namespace std; class MyTime { private: int h ...
- lunix如何查看防火墙是否关闭和关闭开启防火墙命令
查看防火墙是否关闭的命令如下: 1.通过 /etc/init.d/iptables status 或者 service iptables status命令 2.通过 iptables -L命令 查看 ...
- [05]HTML基础之表格标签
1. <table>标签 表格容器,尽量避免用属性书写样式,而是用CSS来表达 border: 数字 //表格边框宽度 2. <caption>标签 表格的标题,一般出现在表格 ...
- Java实现 蓝桥杯 算法训练 My Bad(暴力)
试题 算法训练 My Bad 问题描述 一个逻辑电路将其输入通过不同的门映射到输出,在电路中没有回路.输入和输出是一个逻辑值的有序集合,逻辑值被表示为1和0.我们所考虑的电路由与门(and gate, ...
- Java实现 LeetCode 514 自由之路
514. 自由之路 视频游戏"辐射4"中,任务"通向自由"要求玩家到达名为"Freedom Trail Ring"的金属表盘,并使用表盘拼写 ...
- Java实现 LeetCode 224 基本计算器
224. 基本计算器 实现一个基本的计算器来计算一个简单的字符串表达式的值. 字符串表达式可以包含左括号 ( ,右括号 ),加号 + ,减号 -,非负整数和空格 . 示例 1: 输入: "1 ...
- SQL Server使用Offset/Fetch Next实现分页
T-SQL实现分页 ,查找指定范围内的数据 首先,正常的查询是这样的 使用分页后 select * from Products order by ProductID offset X rows fet ...