【本文版权归微信公众号"代码艺术"(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并发编程的本质是解决这三大问题的更多相关文章

  1. Java并发编程实战 02Java如何解决可见性和有序性问题

    摘要 在上一篇文章当中,讲到了CPU缓存导致可见性.线程切换导致了原子性.编译优化导致了有序性问题.那么这篇文章就先解决其中的可见性和有序性问题,引出了今天的主角:Java内存模型(面试并发的时候会经 ...

  2. Java并发编程——线程安全及解决机制简介

    简介: 本文主要介绍了Java多线程环境下,可能会出现的问题(线程不安全)以及相应的解决措施.通过本文,你将学习到如下几块知识: 1. 为什么需要多线程(多线程的优势) 1. 多线程带来的问题—线程安 ...

  3. Java并发编程实战 03互斥锁 解决原子性问题

    文章系列 Java并发编程实战 01并发编程的Bug源头 Java并发编程实战 02Java如何解决可见性和有序性问题 摘要 在上一篇文章02Java如何解决可见性和有序性问题当中,我们解决了可见性和 ...

  4. Java并发编程实战 04死锁了怎么办?

    Java并发编程文章系列 Java并发编程实战 01并发编程的Bug源头 Java并发编程实战 02Java如何解决可见性和有序性问题 Java并发编程实战 03互斥锁 解决原子性问题 前提 在第三篇 ...

  5. Java并发编程实战 05等待-通知机制和活跃性问题

    Java并发编程系列 Java并发编程实战 01并发编程的Bug源头 Java并发编程实战 02Java如何解决可见性和有序性问题 Java并发编程实战 03互斥锁 解决原子性问题 Java并发编程实 ...

  6. Java并发编程 | 从进程、线程到并发问题实例解决

    计划写几篇文章讲述下Java并发编程,帮助一些初学者成体系的理解并发编程并实际使用,而不只是碎片化的了解一些Synchronized.ReentrantLock等技术点.在讲述的过程中,也想融入一些相 ...

  7. Java并发编程:Synchronized及其实现原理

    Java并发编程系列: Java 并发编程:核心理论 Java并发编程:Synchronized及其实现原理 Java并发编程:Synchronized底层优化(轻量级锁.偏向锁) Java 并发编程 ...

  8. Java并发编程 Volatile关键字解析

    volatile关键字的两层语义 一旦一个共享变量(类的成员变量.类的静态成员变量)被volatile修饰之后,那么就具备了两层语义: 1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了 ...

  9. Java 并发编程:volatile的使用及其原理

    Java并发编程系列: Java 并发编程:核心理论 Java并发编程:Synchronized及其实现原理 Java并发编程:Synchronized底层优化(轻量级锁.偏向锁) Java 并发编程 ...

随机推荐

  1. 如何同时关联多个远程仓库,实现一次 push 多站提交(github + gitee)

    这两天做了简陋轮子,主要想放到npm上, Github: canvas-components Gitee: canvas-components github 上一份,gitee 上一份.(走过路过,s ...

  2. Python操作Word与Excel并打包

    安装模块 # Word操作库 pip install docx # Excel操作库 pip install openpyxl # 打包exe工具 pip install pyinstaller Wo ...

  3. 中文分词工具——jieba

    汉字是智慧和想象力的宝库. --索尼公司创始人井深大 简介 在英语中,单词就是"词"的表达,一个句子是由空格来分隔的,而在汉语中,词以字为基本单位,但是一篇文章的表达是以词来划分的 ...

  4. 【CTFHUB】Web技能树

    Web HTTP协议 请求方式

  5. hackone ssrf

    alyssa_herrera submitted a report to U.S. Dept Of Defense. Jan 29th (2 years ago) Summary:A server s ...

  6. Chisel3 - util - RRArbiter

    https://mp.weixin.qq.com/s/GcNIFkHfa0gW0HKkKvHZEQ     循环优先级(Round Robin)仲裁器.   参考链接: https://github. ...

  7. js 识别二维码

    本文引用analyticCode.js.llqrcode.js实现识别二维码功能 html代码: <div class="box" id="analytic&quo ...

  8. Java实现 蓝桥杯 算法训练 排序

    算法训练 排序 时间限制:1.0s 内存限制:512.0MB 问题描述 编写一个程序,输入3个整数,然后程序将对这三个整数按照从大到小进行排列. 输入格式:输入只有一行,即三个整数,中间用空格隔开. ...

  9. Java实现 LeetCode 44 通配符匹配

    44. 通配符匹配 给定一个字符串 (s) 和一个字符模式 § ,实现一个支持 '?' 和 '*' 的通配符匹配. '?' 可以匹配任何单个字符. '*' 可以匹配任意字符串(包括空字符串). 两个字 ...

  10. 第六届蓝桥杯JavaB组省赛真题

    解题代码部分来自网友,如果有不对的地方,欢迎各位大佬评论 题目1.三角形面积 题目描述 如图1所示.图中的所有小方格面积都是1. 那么,图中的三角形面积应该是多少呢? 请填写三角形的面积.不要填写任何 ...