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

相关文章

在Ubuntu上部署SpringBoot应用的操作步骤

《在Ubuntu上部署SpringBoot应用的操作步骤》随着云计算和容器化技术的普及,Linux服务器已成为部署Web应用程序的主流平台之一,Java作为一种跨平台的编程语言,具有广泛的应用场景,本... 目录一、部署准备二、安装 Java 环境1. 安装 JDK2. 验证 Java 安装三、安装 mys

Springboot的ThreadPoolTaskScheduler线程池轻松搞定15分钟不操作自动取消订单

《Springboot的ThreadPoolTaskScheduler线程池轻松搞定15分钟不操作自动取消订单》:本文主要介绍Springboot的ThreadPoolTaskScheduler线... 目录ThreadPoolTaskScheduler线程池实现15分钟不操作自动取消订单概要1,创建订单后

JAVA中整型数组、字符串数组、整型数和字符串 的创建与转换的方法

《JAVA中整型数组、字符串数组、整型数和字符串的创建与转换的方法》本文介绍了Java中字符串、字符数组和整型数组的创建方法,以及它们之间的转换方法,还详细讲解了字符串中的一些常用方法,如index... 目录一、字符串、字符数组和整型数组的创建1、字符串的创建方法1.1 通过引用字符数组来创建字符串1.2

SpringCloud集成AlloyDB的示例代码

《SpringCloud集成AlloyDB的示例代码》AlloyDB是GoogleCloud提供的一种高度可扩展、强性能的关系型数据库服务,它兼容PostgreSQL,并提供了更快的查询性能... 目录1.AlloyDBjavascript是什么?AlloyDB 的工作原理2.搭建测试环境3.代码工程1.

Java调用Python代码的几种方法小结

《Java调用Python代码的几种方法小结》Python语言有丰富的系统管理、数据处理、统计类软件包,因此从java应用中调用Python代码的需求很常见、实用,本文介绍几种方法从java调用Pyt... 目录引言Java core使用ProcessBuilder使用Java脚本引擎总结引言python

SpringBoot操作spark处理hdfs文件的操作方法

《SpringBoot操作spark处理hdfs文件的操作方法》本文介绍了如何使用SpringBoot操作Spark处理HDFS文件,包括导入依赖、配置Spark信息、编写Controller和Ser... 目录SpringBoot操作spark处理hdfs文件1、导入依赖2、配置spark信息3、cont

springboot整合 xxl-job及使用步骤

《springboot整合xxl-job及使用步骤》XXL-JOB是一个分布式任务调度平台,用于解决分布式系统中的任务调度和管理问题,文章详细介绍了XXL-JOB的架构,包括调度中心、执行器和Web... 目录一、xxl-job是什么二、使用步骤1. 下载并运行管理端代码2. 访问管理页面,确认是否启动成功

Java中的密码加密方式

《Java中的密码加密方式》文章介绍了Java中使用MD5算法对密码进行加密的方法,以及如何通过加盐和多重加密来提高密码的安全性,MD5是一种不可逆的哈希算法,适合用于存储密码,因为其输出的摘要长度固... 目录Java的密码加密方式密码加密一般的应用方式是总结Java的密码加密方式密码加密【这里采用的

Java中ArrayList的8种浅拷贝方式示例代码

《Java中ArrayList的8种浅拷贝方式示例代码》:本文主要介绍Java中ArrayList的8种浅拷贝方式的相关资料,讲解了Java中ArrayList的浅拷贝概念,并详细分享了八种实现浅... 目录引言什么是浅拷贝?ArrayList 浅拷贝的重要性方法一:使用构造函数方法二:使用 addAll(

解决mybatis-plus-boot-starter与mybatis-spring-boot-starter的错误问题

《解决mybatis-plus-boot-starter与mybatis-spring-boot-starter的错误问题》本文主要讲述了在使用MyBatis和MyBatis-Plus时遇到的绑定异常... 目录myBATis-plus-boot-starpythonter与mybatis-spring-b