JAVA多线程基础--------并发编程三大特性(原子性、可见性、有序性)

2024-01-28 04:08

本文主要是介绍JAVA多线程基础--------并发编程三大特性(原子性、可见性、有序性),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

并发编程三大特性的定义和由来

凡事有因才有果,有果必有因,并发编程的三大特性也如此,人们不会莫名其妙定义出并发编程的三大特性。接下来我们探讨下为什么会有并发编程这三大特性?


简单地说,并发编程这三大特性就是为了在多个线程交替执行任务的过程中保证线程安全性(点此跳转)。那么为什么会出现线程不安全的现象呢?接下来我们从这三个特性切入来介绍线程不安全的原因。以下涉及到的主内存工作内存相当于主存cpu缓存,详见Java内存模型


  • 原子性:一组操作要么全部执行,要么全部不执行,执行过程中不能被中断。
    Java并发编程中必然存在多个线程的交替执行,因此不论采取何种线程调度算法,都会涉及到线程的切换,而在线程切换的过程中,如果对某个共享变量的操作不是原子的,就可能会导致脏读等各种数据混乱的问题,造成线程不安全,因此我们必须保证对共享变量操作的原子性防止数据混乱以保证线程安全
    在这里插入图片描述
  • 可见性:一个线程修改了某个共享变量,其他线程立即可以“感知到”
    从对Java内存模型的了解我们可以知道,Java中每个线程对共享数据的修改都是在其工作内存中进行的,而每个线程在其工作内存中对共享数据的修改并不会立即同步到主内存,因此其他线程并不能立即“感知到”某个线程对共享数据的修改,这样就会导致每个线程工作内存中同一个共享变量的值不一定相等,即缓存不一致,导致线程不安全。因此我们必须保证可见性以保证线程安全
    在这里插入图片描述
  • 有序性:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。
    为了提高性能,编译器和处理器可能会在满足数据依赖性(如a+=1;a*=2这两个操作不能交换顺序,一旦交换会影响程序的执行结果,即在单线程环境中,对指令的重排序并不影响执行结果) 的条件下对操作进行重新排序。在单线程环境下,这种重排序不会有什么问题,因为执行结果总是正确的,但是在多线程环境下就会出现问题,看一个例子:
public class Test{private char[] configText;private boolean init = false;//假设以下代码在线程A中执行public void configer(){configText = readConfigFile();    //代码1init = true;   //通知其他线程配置可用    代码2}//假设以下代码在线程B中执行public void work() throws Exception{while(!init){   Thread.sleep(10000);}use(configText);}
}

上面这段程序中代码1和代码2这两行的实际执行顺序可能会发生交换,这种情况就会导致配置信息还未完全配置好时,其他线程就开始使用这个配置信息,这显然是不正确的。因此我们必须对指令重排序进行一定程度的限制以保证线程安全

总结一下,之所以会出现并发编程的三大特性,就是因为在提升程序性能的同时需要保证安全性,而原子性、可见性、有序性这三大特性可以认为是线程安全的等价概念,我们需要通过一些机制来保证这三大特性,也就是保证线程安全

保证并发编程三大特性的机制

上文说到之所以会出现并发编程的三大特性,就是因为在提升程序性能的同时需要保证安全性,而保证原子性、可见性、有序性这三大特性可以认为是保证线程安全的等价概念,我们需要通过一些机制来保证这三大特性,也就是保证线程安全。那么都有哪些机制可以保证这三大特性呢?下面我们一一举例介绍

原子性
首先我们明确一点,Java内存模型保证了对基本数据类型的访问、读写都是具备原子性的(除了非volatile类型的long和double型变量,事实上JVM允许将64位的读操作或写操作分解为两个32位操作,这点我们在后续文章中详细介绍),但是这仅仅是小范围的原子性保证,在很多场景下我们需要更大范围的原子性保证(例如每个线程的任务是将共享变量先自增,再乘10),这种情况下,Java内存模型直接提供的原子性保证已不足以保证线程安全了。这时候就需要用关键字synchronized来保证更大范围的原子性。


  • synchronized
    Java内存模型提供了lock和unlock操作来满足更大范围的原子性,JVM并未把lock和unlock操作直接开放给用户,但更高层次的字节码指令monitorenter和monitorexit可以隐式地使用这两个操作,而这两个字节码指令映射到Java代码中就是synchronized同步块
    用一个不是很恰当的图可以说明这个问题
    在这里插入图片描述

看一个例子:假设有十个线程,每个线程执行一次increase方法,最终的结果有极大概率小于10,因为inc++是非原子操作

public class INS{public static int inc = 0;public static void increase(){inc++;   //非原子操作(读取-赋值-写入)}
}

改进: 使用 synchronized关键字

public class INS{public static int inc = 0;public static void increase(){synchronized (INS.class){inc++;}}
}

可见性:


机制1:使用volatile(底层原理点此了解)型变量的特殊规则保证新值能立即同步到主存,以及每次使用前立即从主存内刷新
机制2:使用synchronized关键字,上文提到退出synchronized同步块时相当于执行unkock操作,而JVM规定对一个共享变量执行unlock操作之前,必须先把此共享变量同步回主内存中,以供其他使用该共享变量的线程可读取到正确的值。
机制3:被final修饰的字段在构造器中一旦初始化完成,并且在构造过程中没有把对象的this引用传递出去(构造过程中一旦将this引用传出,其他线程就会得到一个构造了一半的对象的引用,这样是非常不安全的),那么在其他线程中就能看见final字段的值。


看一个例子:

public class Test{private boolean flag = false;//假设以下代码正在由线程B执行public void change(){flag = true;}//假设以下代码正在由线程A执行public void doWork(){while(!flag){............}}
}

如果A线程正在执行doWork,B线程执行了change将flag的状态改为true,这时,A线程并不会立即退出循环,因为B线程对flag的修改是在它的工作内存中进行的,并不会立即写回主存

改进1:

public class Test{private volatile boolean flag = false;//假设以下代码正在由线程B执行public void change(){flag = true;}//假设以下代码正在由线程A执行public void doWork(){while(!flag){............}}
}

改进2:

public class Test{private boolean flag = false;//假设以下代码正在由线程B执行synchronized public void change(){flag = true;}//假设以下代码正在由线程A执行public void doWork(){while(!flag){............}}
}

保证可见性主要通过以上两种方法,很少使用final关键字保证可见性,这里不举例,但是要知道final有这个功能
有序性:


机制1:使用volatile(底层原理点此了解)关键字保证有序性,它的原理是使用内存屏障禁止指令重排序
机制2:使用synchronized关键字保证有序性。值得注意的是,synchronized关键字并不能禁止指令重排序,上文提到进入synchronized同步块相当于执行lock操作,而JVM规定一个共享变量在同一个时刻只允许一条线程对其进行lock操作,相当于synchronized同步块里的代码在每个时刻都是单线程执行的,因此即使其内部代码进行重排序,也不影响结果。


看一个例子:

public class Test{private char[] configText;private boolean init = false;//假设以下代码在线程A中执行public void configer(){configText = readConfigFile();    //代码1init = true;   //通知其他线程配置可用    代码2}//假设以下代码在线程B中执行public void work() throws Exception{while(!init){   Thread.sleep(10000);}use(configText);}
}

上面这段程序中代码1和代码2这两行的实际执行顺序可能会发生交换,这种情况就会导致配置信息还未完全配置好时,其他线程就开始使用这个配置信息,这显然是不正确的。

改进1

public class Test{private char[] configText;private volatile boolean init = false;//假设以下代码在线程A中执行public void configer(){configText = readConfigFile();    //代码1init = true;   //通知其他线程配置可用    代码2}//假设以下代码在线程B中执行public void work() throws Exception{while(!init){   Thread.sleep(10000);}use(configText);}
}

将init变量声明为volatile型,代码1和代码2不会进行指令重排序,也就避免了上面的问题

改进2

public class Test{private char[] configText;private boolean init = false;//假设以下代码在线程A中执行synchronized public void configer(){configText = readConfigFile();    //代码1init = true;   //通知其他线程配置可用    代码2}//假设以下代码在线程B中执行public void work() throws Exception{while(!init){   Thread.sleep(10000);}synchronized(this){use(configText);}}
}

加上synchronized后,并不能禁止代码1和代码2的重排序。但是,代码1和代码2在同一时刻只能由一个线程执行,且必须等该线程执行完代码1和代码2,别的线程才能进入synchronized同步代码块,因此,可以认为configer方法是在单线程环境下执行的,即使进行了指令重排序也不影响最终结果(参考上文有序性的定义和由来)。如果代码2先执行,那也会等代码1执行完,别的线程才能进入synchronized代码块执行work方法,使用configText。

总结一下,通过上面对保证并发编程三大特性的机制的介绍可以看出,仅用synchronized关键字就可以保证原子性、可见性和有序性,足以保证线程安全。但一定不能滥用synchronized关键字,否则可能导致程序性能降低和死锁、饥饿等活跃性问题

这篇关于JAVA多线程基础--------并发编程三大特性(原子性、可见性、有序性)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/652429

相关文章

java如何解压zip压缩包

《java如何解压zip压缩包》:本文主要介绍java如何解压zip压缩包问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Java解压zip压缩包实例代码结果如下总结java解压zip压缩包坐在旁边的小伙伴问我怎么用 java 将服务器上的压缩文件解压出来,

SpringBoot中SM2公钥加密、私钥解密的实现示例详解

《SpringBoot中SM2公钥加密、私钥解密的实现示例详解》本文介绍了如何在SpringBoot项目中实现SM2公钥加密和私钥解密的功能,通过使用Hutool库和BouncyCastle依赖,简化... 目录一、前言1、加密信息(示例)2、加密结果(示例)二、实现代码1、yml文件配置2、创建SM2工具

Spring WebFlux 与 WebClient 使用指南及最佳实践

《SpringWebFlux与WebClient使用指南及最佳实践》WebClient是SpringWebFlux模块提供的非阻塞、响应式HTTP客户端,基于ProjectReactor实现,... 目录Spring WebFlux 与 WebClient 使用指南1. WebClient 概述2. 核心依

Spring Boot @RestControllerAdvice全局异常处理最佳实践

《SpringBoot@RestControllerAdvice全局异常处理最佳实践》本文详解SpringBoot中通过@RestControllerAdvice实现全局异常处理,强调代码复用、统... 目录前言一、为什么要使用全局异常处理?二、核心注解解析1. @RestControllerAdvice2

Spring IoC 容器的使用详解(最新整理)

《SpringIoC容器的使用详解(最新整理)》文章介绍了Spring框架中的应用分层思想与IoC容器原理,通过分层解耦业务逻辑、数据访问等模块,IoC容器利用@Component注解管理Bean... 目录1. 应用分层2. IoC 的介绍3. IoC 容器的使用3.1. bean 的存储3.2. 方法注

Spring事务传播机制最佳实践

《Spring事务传播机制最佳实践》Spring的事务传播机制为我们提供了优雅的解决方案,本文将带您深入理解这一机制,掌握不同场景下的最佳实践,感兴趣的朋友一起看看吧... 目录1. 什么是事务传播行为2. Spring支持的七种事务传播行为2.1 REQUIRED(默认)2.2 SUPPORTS2

怎样通过分析GC日志来定位Java进程的内存问题

《怎样通过分析GC日志来定位Java进程的内存问题》:本文主要介绍怎样通过分析GC日志来定位Java进程的内存问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、GC 日志基础配置1. 启用详细 GC 日志2. 不同收集器的日志格式二、关键指标与分析维度1.

Java进程异常故障定位及排查过程

《Java进程异常故障定位及排查过程》:本文主要介绍Java进程异常故障定位及排查过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、故障发现与初步判断1. 监控系统告警2. 日志初步分析二、核心排查工具与步骤1. 进程状态检查2. CPU 飙升问题3. 内存

java中新生代和老生代的关系说明

《java中新生代和老生代的关系说明》:本文主要介绍java中新生代和老生代的关系说明,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、内存区域划分新生代老年代二、对象生命周期与晋升流程三、新生代与老年代的协作机制1. 跨代引用处理2. 动态年龄判定3. 空间分

Java设计模式---迭代器模式(Iterator)解读

《Java设计模式---迭代器模式(Iterator)解读》:本文主要介绍Java设计模式---迭代器模式(Iterator),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,... 目录1、迭代器(Iterator)1.1、结构1.2、常用方法1.3、本质1、解耦集合与遍历逻辑2、统一