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

相关文章

SpringBoot UserAgentUtils获取用户浏览器的用法

《SpringBootUserAgentUtils获取用户浏览器的用法》UserAgentUtils是于处理用户代理(User-Agent)字符串的工具类,一般用于解析和处理浏览器、操作系统以及设备... 目录介绍效果图依赖封装客户端工具封装IP工具实体类获取设备信息入库介绍UserAgentUtils

Spring 中的循环引用问题解决方法

《Spring中的循环引用问题解决方法》:本文主要介绍Spring中的循环引用问题解决方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录什么是循环引用?循环依赖三级缓存解决循环依赖二级缓存三级缓存本章来聊聊Spring 中的循环引用问题该如何解决。这里聊

Java学习手册之Filter和Listener使用方法

《Java学习手册之Filter和Listener使用方法》:本文主要介绍Java学习手册之Filter和Listener使用方法的相关资料,Filter是一种拦截器,可以在请求到达Servl... 目录一、Filter(过滤器)1. Filter 的工作原理2. Filter 的配置与使用二、Listen

Spring Boot中JSON数值溢出问题从报错到优雅解决办法

《SpringBoot中JSON数值溢出问题从报错到优雅解决办法》:本文主要介绍SpringBoot中JSON数值溢出问题从报错到优雅的解决办法,通过修改字段类型为Long、添加全局异常处理和... 目录一、问题背景:为什么我的接口突然报错了?二、为什么会发生这个错误?1. Java 数据类型的“容量”限制

Java对象转换的实现方式汇总

《Java对象转换的实现方式汇总》:本文主要介绍Java对象转换的多种实现方式,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录Java对象转换的多种实现方式1. 手动映射(Manual Mapping)2. Builder模式3. 工具类辅助映

SpringBoot请求参数接收控制指南分享

《SpringBoot请求参数接收控制指南分享》:本文主要介绍SpringBoot请求参数接收控制指南,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Spring Boot 请求参数接收控制指南1. 概述2. 有注解时参数接收方式对比3. 无注解时接收参数默认位置

SpringBoot基于配置实现短信服务策略的动态切换

《SpringBoot基于配置实现短信服务策略的动态切换》这篇文章主要为大家详细介绍了SpringBoot在接入多个短信服务商(如阿里云、腾讯云、华为云)后,如何根据配置或环境切换使用不同的服务商,需... 目录目标功能示例配置(application.yml)配置类绑定短信发送策略接口示例:阿里云 & 腾

SpringBoot项目中报错The field screenShot exceeds its maximum permitted size of 1048576 bytes.的问题及解决

《SpringBoot项目中报错ThefieldscreenShotexceedsitsmaximumpermittedsizeof1048576bytes.的问题及解决》这篇文章... 目录项目场景问题描述原因分析解决方案总结项目场景javascript提示:项目相关背景:项目场景:基于Spring

Spring Boot 整合 SSE的高级实践(Server-Sent Events)

《SpringBoot整合SSE的高级实践(Server-SentEvents)》SSE(Server-SentEvents)是一种基于HTTP协议的单向通信机制,允许服务器向浏览器持续发送实... 目录1、简述2、Spring Boot 中的SSE实现2.1 添加依赖2.2 实现后端接口2.3 配置超时时

Spring Boot读取配置文件的五种方式小结

《SpringBoot读取配置文件的五种方式小结》SpringBoot提供了灵活多样的方式来读取配置文件,这篇文章为大家介绍了5种常见的读取方式,文中的示例代码简洁易懂,大家可以根据自己的需要进... 目录1. 配置文件位置与加载顺序2. 读取配置文件的方式汇总方式一:使用 @Value 注解读取配置方式二