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集成easypoi导出word换行处理过程

《springboot集成easypoi导出word换行处理过程》SpringBoot集成Easypoi导出Word时,换行符n失效显示为空格,解决方法包括生成段落或替换模板中n为回车,同时需确... 目录项目场景问题描述解决方案第一种:生成段落的方式第二种:替换模板的情况,换行符替换成回车总结项目场景s

SpringBoot集成redisson实现延时队列教程

《SpringBoot集成redisson实现延时队列教程》文章介绍了使用Redisson实现延迟队列的完整步骤,包括依赖导入、Redis配置、工具类封装、业务枚举定义、执行器实现、Bean创建、消费... 目录1、先给项目导入Redisson依赖2、配置redis3、创建 RedissonConfig 配

SpringBoot中@Value注入静态变量方式

《SpringBoot中@Value注入静态变量方式》SpringBoot中静态变量无法直接用@Value注入,需通过setter方法,@Value(${})从属性文件获取值,@Value(#{})用... 目录项目场景解决方案注解说明1、@Value("${}")使用示例2、@Value("#{}"php

SpringBoot分段处理List集合多线程批量插入数据方式

《SpringBoot分段处理List集合多线程批量插入数据方式》文章介绍如何处理大数据量List批量插入数据库的优化方案:通过拆分List并分配独立线程处理,结合Spring线程池与异步方法提升效率... 目录项目场景解决方案1.实体类2.Mapper3.spring容器注入线程池bejsan对象4.创建

线上Java OOM问题定位与解决方案超详细解析

《线上JavaOOM问题定位与解决方案超详细解析》OOM是JVM抛出的错误,表示内存分配失败,:本文主要介绍线上JavaOOM问题定位与解决方案的相关资料,文中通过代码介绍的非常详细,需要的朋... 目录一、OOM问题核心认知1.1 OOM定义与技术定位1.2 OOM常见类型及技术特征二、OOM问题定位工具

基于 Cursor 开发 Spring Boot 项目详细攻略

《基于Cursor开发SpringBoot项目详细攻略》Cursor是集成GPT4、Claude3.5等LLM的VSCode类AI编程工具,支持SpringBoot项目开发全流程,涵盖环境配... 目录cursor是什么?基于 Cursor 开发 Spring Boot 项目完整指南1. 环境准备2. 创建

Spring Security简介、使用与最佳实践

《SpringSecurity简介、使用与最佳实践》SpringSecurity是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架,本文给大家介绍SpringSec... 目录一、如何理解 Spring Security?—— 核心思想二、如何在 Java 项目中使用?——

SpringBoot+RustFS 实现文件切片极速上传的实例代码

《SpringBoot+RustFS实现文件切片极速上传的实例代码》本文介绍利用SpringBoot和RustFS构建高性能文件切片上传系统,实现大文件秒传、断点续传和分片上传等功能,具有一定的参考... 目录一、为什么选择 RustFS + SpringBoot?二、环境准备与部署2.1 安装 RustF

springboot中使用okhttp3的小结

《springboot中使用okhttp3的小结》OkHttp3是一个JavaHTTP客户端,可以处理各种请求类型,比如GET、POST、PUT等,并且支持高效的HTTP连接池、请求和响应缓存、以及异... 在 Spring Boot 项目中使用 OkHttp3 进行 HTTP 请求是一个高效且流行的方式。

MySQL的JDBC编程详解

《MySQL的JDBC编程详解》:本文主要介绍MySQL的JDBC编程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录前言一、前置知识1. 引入依赖2. 认识 url二、JDBC 操作流程1. JDBC 的写操作2. JDBC 的读操作总结前言本文介绍了mysq