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中流式并行操作parallelStream的原理和使用方法

《Java中流式并行操作parallelStream的原理和使用方法》本文详细介绍了Java中的并行流(parallelStream)的原理、正确使用方法以及在实际业务中的应用案例,并指出在使用并行流... 目录Java中流式并行操作parallelStream0. 问题的产生1. 什么是parallelS

Java中Redisson 的原理深度解析

《Java中Redisson的原理深度解析》Redisson是一个高性能的Redis客户端,它通过将Redis数据结构映射为Java对象和分布式对象,实现了在Java应用中方便地使用Redis,本文... 目录前言一、核心设计理念二、核心架构与通信层1. 基于 Netty 的异步非阻塞通信2. 编解码器三、

SpringBoot基于注解实现数据库字段回填的完整方案

《SpringBoot基于注解实现数据库字段回填的完整方案》这篇文章主要为大家详细介绍了SpringBoot如何基于注解实现数据库字段回填的相关方法,文中的示例代码讲解详细,感兴趣的小伙伴可以了解... 目录数据库表pom.XMLRelationFieldRelationFieldMapping基础的一些代

一篇文章彻底搞懂macOS如何决定java环境

《一篇文章彻底搞懂macOS如何决定java环境》MacOS作为一个功能强大的操作系统,为开发者提供了丰富的开发工具和框架,下面:本文主要介绍macOS如何决定java环境的相关资料,文中通过代码... 目录方法一:使用 which命令方法二:使用 Java_home工具(Apple 官方推荐)那问题来了,

Java HashMap的底层实现原理深度解析

《JavaHashMap的底层实现原理深度解析》HashMap基于数组+链表+红黑树结构,通过哈希算法和扩容机制优化性能,负载因子与树化阈值平衡效率,是Java开发必备的高效数据结构,本文给大家介绍... 目录一、概述:HashMap的宏观结构二、核心数据结构解析1. 数组(桶数组)2. 链表节点(Node

Java AOP面向切面编程的概念和实现方式

《JavaAOP面向切面编程的概念和实现方式》AOP是面向切面编程,通过动态代理将横切关注点(如日志、事务)与核心业务逻辑分离,提升代码复用性和可维护性,本文给大家介绍JavaAOP面向切面编程的概... 目录一、AOP 是什么?二、AOP 的核心概念与实现方式核心概念实现方式三、Spring AOP 的关

详解SpringBoot+Ehcache使用示例

《详解SpringBoot+Ehcache使用示例》本文介绍了SpringBoot中配置Ehcache、自定义get/set方式,并实际使用缓存的过程,文中通过示例代码介绍的非常详细,对大家的学习或者... 目录摘要概念内存与磁盘持久化存储:配置灵活性:编码示例引入依赖:配置ehcache.XML文件:配置

Java 虚拟线程的创建与使用深度解析

《Java虚拟线程的创建与使用深度解析》虚拟线程是Java19中以预览特性形式引入,Java21起正式发布的轻量级线程,本文给大家介绍Java虚拟线程的创建与使用,感兴趣的朋友一起看看吧... 目录一、虚拟线程简介1.1 什么是虚拟线程?1.2 为什么需要虚拟线程?二、虚拟线程与平台线程对比代码对比示例:三

从基础到高级详解Go语言中错误处理的实践指南

《从基础到高级详解Go语言中错误处理的实践指南》Go语言采用了一种独特而明确的错误处理哲学,与其他主流编程语言形成鲜明对比,本文将为大家详细介绍Go语言中错误处理详细方法,希望对大家有所帮助... 目录1 Go 错误处理哲学与核心机制1.1 错误接口设计1.2 错误与异常的区别2 错误创建与检查2.1 基础

Java中的.close()举例详解

《Java中的.close()举例详解》.close()方法只适用于通过window.open()打开的弹出窗口,对于浏览器的主窗口,如果没有得到用户允许是不能关闭的,:本文主要介绍Java中的.... 目录当你遇到以下三种情况时,一定要记得使用 .close():用法作用举例如何判断代码中的 input