java并发编程:可见性、原子性、有序性三大特性详解

news2024/12/24 9:43:03

文章目录

  • 可见性
    • 导致可见性的原因
      • 线程交叉执行
      • 重排序结合线程交叉执行
      • 共享变量更新后没有及时更新
    • 如何解决可见性问题
  • 原子性
    • 出现原子性问题的原因
    • 如何解决原子性问题
  • 有序性
    • 导致有序性的原因
    • 如何解决有序性问题
  • 总结


可见性

内存可见性,指的是线程之间的可见性,当一个线程修改了共享变量时,另一个线程可以读取到这个修改后的值。

导致可见性的原因

线程交叉执行

线程交叉执行多数情况是由于线程切换导致的,例如下图中的线程A在执行过程中切换到线程B执行完成后,再切换回线程A执行剩下的操作;此时线程B对变量的修改不能对线程A立即可见,这就导致了计算结果和理想结果不一致的情况。

img

重排序结合线程交叉执行

例如下面这段代码

int a = 0;    //行1
int b = 0;    //行2
a = b + 10;   //行3
b = a + 9;    //行4

如果行1和行2在编译的时候改变顺序,执行结果不会受到影响;

如果将行3和行4在变异的时候交换顺序,执行结果就会受到影响,因为b的值得不到预期的19;

img

由图知:由于编译时改变了执行顺序,导致结果不一致;而两个线程的交叉执行又导致线程改变后的结果也不是预期值,简直雪上加霜!

共享变量更新后没有及时更新

因为主线程对共享变量的修改没有及时更新,子线程中不能立即得到最新值,导致程序不能按照预期结果执行。

例如下面这段代码:

public class Visibility {

    // 状态标识flag
    private static boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        System.out.println(LocalDateTime.now() + "主线程启动计数子线程");
        new CountThread().start();

        Thread.sleep(100);
        // 设置flag为false,使上面启动的子线程跳出while循环,结束运行
        Visibility.flag = false;
        System.out.println(LocalDateTime.now() + "主线程将状态标识flag被置为false了");
    }

    static class CountThread extends Thread {
        @Override
        public void run() {
            System.out.println(LocalDateTime.now() + "计数子线程start计数");
            int i = 0;
            while (Visibility.flag) {
                i++;
            }
            System.out.println(LocalDateTime.now() + "计数子线程end计数,运行结束:i的值是" + i);
        }
    }

}

运行结果是:

在这里插入图片描述

从控制台的打印结果可以看出,因为主线程对flag的修改,对计数子线程没有立即可见,所以导致了计数子线程久久不能跳出while循环,结束子线程。

如何解决可见性问题

1、volatile关键字

volatile关键字能保证可见性,但也只能保证可见性,在此处就能保证flag的修改能立即被计数子线程获取到。

此时纠正上面例子出现的问题,只需在定义全局变量的时候加上volatile关键字

// 状态标识flag    
private static volatile boolean flag = true;

2、Atomic相关类

将标识状态flag在定义的时候使用Atomic相关类来进行定义的话,就能很好的保证flag属性的可见性以及原子性。

此时纠正上面例子出现的问题,只需在定义全局变量的时候将变量定义成Atomic相关类

// 状态标识flag    
private static AtomicBoolean flag = new AtomicBoolean(true); 

不过值得注意的一点是,此时原子类相关的方法设置新值和得到值的放的是有点变化,如下:

// 设置flag的值   
VisibilityDemo.flag.set(false);        
// 获取flag的值    
VisibilityDemo.flag.get() 

3、锁

此处我们使用的是Java常见的synchronized关键字。

此时纠正上面例子出现的问题,只需在为计数操作i++添加synchronized关键字修饰。

synchronized (this) {    
    i++;    
} 

通过上面三种方式,都得到类似如下的期望结果:

在这里插入图片描述

原子性

一个或者多个操作在 CPU 执行的过程中不被中断的特性。

出现原子性问题的原因

导致共享变量在线程之间出现原子性问题的原因是上下文切换。

那么接下来,我们通过一个例子来重现原子性问题。

package td;

import java.util.ArrayList;
import java.util.List;

/**
 * 演示:原子性问题 -> 指当一个线程对共享变量操作到一半时,另外一个线程也有可能来操作共享变量,干扰了第一个线程的操作
 */
public class Atomicity {

    //定义一个共享变量
    private static int number = 0;

    public static void addNumber(){
        number++;
    }

    public static void main(String[] args) throws InterruptedException {
        //对number进行1000的++
        Runnable runnable = () -> {
            for (int i = 0; i < 1000; i++) {
                addNumber();
            }
        };

        List<Thread> list = new ArrayList<>();
        //使用10个线程来进行操作
        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(runnable);
            t.start();
            list.add(t);
        }

        for (Thread t : list) {
            //t.join()方法只会使主线程进入等待池并等待t线程执行完毕后才会被唤醒。并不影响同一时刻处在运行状态的其他线程
            t.join();
        }

        System.out.println("number = " + number);
    }
}

多次运行上面的程序,也有我们期望的结果 number = 10000,当时会出现不是我们想要的结果。

出现上面情况的原因就是因为:

 public static void addNumber(){
        number++;
    }

这段代码并不是原子操作,其中的number是一个共享变量。在多线程环境下可能会被打断。就这样原子性问题就赤裸裸的出现了。

如何解决原子性问题

1、synchronized关键字

synchronized既可以保证操作的可见性,也可以保证操作结果的原子性。

所以,此处我们只需要将addNumber()方法设置成synchronized的就能保证原子性了。

public synchronized static void addNumber(){
    number++;
}

2、Lock锁

static Lock lock = new ReentrantLock();

public static void addNumber(){
    lock.lock();//加锁
    try{
        number++;
    }finally {
        lock.unlock();//释放锁
    }
}

Lock锁保证原子性的原理和synchronized类似

3、原子操作类型

JDK提供了很多原子操作类来保证操作的原子性。比如最常见的基本类型:

AtomicBoolean
AtomicLong
AtomicDouble
AtomicInteger

这些原子操作类的底层是使用CAS机制的,这个机制保证了整个赋值操作是原子的不能被打断的,从而保证了最终结果的正确性。

和synchronized相比,原子操作类型相当于是从微观上保证原子性,而synchronized是从宏观上保证原子性。

public class Atomicity {

    //定义一个共享变量
    private static AtomicInteger number = new AtomicInteger();

    public static void add(){
        number.incrementAndGet();
    }

    public static int get(){
        return number.get();
    }

    public static void main(String[] args) throws InterruptedException {
        //对number进行1000的++
        Runnable runnable = () -> {
            for (int i = 0; i < 1000; i++) {
                add();
            }
        };

        List<Thread> list = new ArrayList<>();
        //使用5个线程来进行操作
        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(runnable);
            t.start();
            list.add(t);
        }

        for (Thread t : list) {
            //t.join()方法只会使主线程进入等待池并等待t线程执行完毕后才会被唤醒。并不影响同一时刻处在运行状态的其他线程
            t.join();
        }

        System.out.println("number = " + get());
    }
}

有序性

有序性问题 指的是在多线程环境下(多核),由于执行语句重排序后,重排序的这一部分没有一起执行完,就切换到了其它线程,导致的结果与预期不符的问题。这就是编译器的编译优化给并发编程带来的程序有序性问题。

用图示就是:

img

导致有序性的原因

如果一个线程写入值到字段 a,然后写入值到字段 b ,而且b的值不依赖于 a 的值,那么,处理器就能够自由的调整它们的执行顺序,而且缓冲区能够在 a 之前刷新b的值到主内存。此时就可能会出现有序性问题。

例子:

public class Order {

    static int value = 1;
    
    private static boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 500; i++) {
            value = 1;
            flag = false;
            Thread thread1 = new DisplayThread();
            Thread thread2 = new CountThread();
            thread1.start();
            thread2.start();
            System.out.println("=========================================================");
            Thread.sleep(4000);
        }
    }

    static class DisplayThread extends Thread {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + " DisplayThread begin, time:" + LocalDateTime.now());
            value = 1024;
            System.out.println(Thread.currentThread().getName() + " change flag, time:" + LocalDateTime.now());
            flag = true;
            System.out.println(Thread.currentThread().getName() + " DisplayThread end, time:" + LocalDateTime.now());
        }
    }

    static class CountThread extends Thread {
        @Override
        public void run() {
            if (flag) {
                System.out.println(Thread.currentThread().getName() + " value的值是:" + value + ", time:" + LocalDateTime.now());
                System.out.println(Thread.currentThread().getName() + " CountThread flag is true,  time:" + LocalDateTime.now());
            } else {
                System.out.println(Thread.currentThread().getName() + " value的值是:" + value + ", time:" + LocalDateTime.now());
                System.out.println(Thread.currentThread().getName() + " CountThread flag is false, time:" + LocalDateTime.now());
            }
        }
    }
}

运行结果:

在这里插入图片描述

从打印的可以看出:在 DisplayThread 线程执行的时候肯定是发生了重排序,导致先为 flag 赋值,然后切换到 CountThread 线程,这才出现了打印的 value 值是1,falg 值是 true 的情况,再为 value 赋值;不过出现这种情况的原因就是这两个赋值语句之间没有联系,所以编译器在进行代码编译的时候就可能进行指令重排序。

用图示,则为:

img

如何解决有序性问题

1、volatile关键字

volatile 的底层是使用内存屏障来保证有序性的(让一个Cpu缓存中的状态(变量)对其他Cpu缓存可见的一种技术)。

volatile 变量有条规则是指对一个 volatile 变量的写操作, Happens-Before 于后续对这个 volatile 变量的读操作。并且这个规则具有传递性,也就是说:

使用 volatile 修饰flag就可以避免重排序和内存可见性问题。写 volatile 变量时,可以确保 volatile 写之前的操作不会被编译器重排序到 volatile 写之后。读 volatile 变量时,可以确保 volatile 读之后的操作不会被编译器重排序到 volatile 读之前。

img

此时,我们定义变量 flag 时使用 volatile 关键字修饰,如:

    private static volatile boolean flag = false; 

此时,变量的含义是这样子的:

img

也就是说,只要读取到 flag=true; 就能读取到 value=1024;否则就是读取到 flag=false;value=1 的还没被修改过的初始状态;

在这里插入图片描述

但也有可能会出现线程切换带来的原子性问题,就是读取到 flag=false;value=1024 的情况。

在这里插入图片描述

2、加锁

此处我们直接采用Java语言内置的关键字 synchronized,为可能会重排序的部分加锁,让其在宏观上或者说执行结果上看起来没有发生重排序。

代码修改也很简单,只需用 synchronized 关键字修饰run方法即可,代码如下:

public synchronized void run() {
    value = 1024;
    flag = true;
}

总结

最后,简单总结下几种解决方案之间的区别:

特性Atomic变量volatile关键字Lock接口synchronized关键字
原子性可以保障无法保障可以保障可以保障
可见性可以保障可以保障可以保障可以保障
有序性无法保障一定程度保障可以保障可以保障

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/612909.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

IPv6NAT-PT实验:IPv4和IPv6地址转换的配置和验证

IPv6NAT-PT实验&#xff1a;IPv4和IPv6地址转换的配置和验证 【实验目的】 熟悉IPv6NAT-PT的概念。 掌握静态IPv6NAT-PT的配置 掌握动态IPv6NAT-PT的配置。 验证配置。 【实验拓扑】 设备参数如下表所示。 设备 接口 IP地址 子网掩码 默认网关 R1 S0/0 192.168.12…

2023HW护网红队必备工具总结

一、信息收集 1、AppInfoScanner 一款适用于以HVV行动/红队/渗透测试团队为场景的移动端(Android、iOS、WEB、H5、静态网站)信息收集扫描工具&#xff0c;可以帮助渗透测试工程师、红队成员快速收集到移动端或者静态WEB站点中关键的资产信息并提供基本的信息输出,如&#xff…

Java设计模式—模板方法模式

前言&#xff1a;模板方法模式是模板模式的一个具体实现&#xff0c;它定义了一个抽象类&#xff0c;其中包含一个模板方法和若干个基本方法。其中模板方法定义了算法骨架&#xff0c;而基本方法则由子类来实现。因此&#xff0c;模板方法在定义算法的结构方面提供了支持&#…

springMvc 解决 multipart/form-data 方式提交请求 不能获取非文件类型参数的问题和指定springmvc获取静态资源路径

问题&#xff1a; RequestMapping(value "/test",method RequestMethod.POST)ResponseBodypublic String test(String name,String id,MultipartFile file){System.out.println(name);System.out.println(id);System.out.println(file.getOriginalFilename());ret…

onnx模型的修改与调试demo

主要参考&#xff1a; 模型部署入门教程&#xff08;五&#xff09;&#xff1a;ONNX 模型的修改与调试 第五章&#xff1a;ONNX 模型的修改与调试 使用netron 可视化模型 读写onnx 构造onnx 创建一个描述线性函数 output axb 的onnx模型。 需要两个节点&#xff0c;第一个…

造轮子--自己封装一个 start

背景 都说不要造轮子&#xff0c;但是青柠认为&#xff0c;有的时候&#xff0c;造个轮子&#xff0c;更有助于清楚详细的业务逻辑&#xff0c;所以咱也自己写一个轮子&#xff0c;搞个系统开发。大体思路就是先搭建基础框架&#xff0c;然后细写业务逻辑&#xff0c;会涵盖主…

图解HTTP书籍学习2

确保Web安全的HTTPS HTTP的缺点 ●通信使用明文&#xff08;不加密&#xff09;&#xff0c;内容可能会被窃听 ●不验证通信方的身份&#xff0c;因此有可能遭遇伪装 ●无法证明报文的完整性&#xff0c;所以有可能已遭篡改 加密处理防止被窃听 通信的加密 一种方式就是…

【React】类组件,JSX语法,JSX原理,传递参数,条件渲染,列表渲染

❤️ Author&#xff1a; 老九 ☕️ 个人博客&#xff1a;老九的CSDN博客 &#x1f64f; 个人名言&#xff1a;不可控之事 乐观面对 &#x1f60d; 系列专栏&#xff1a; 文章目录 React使用组件&#xff08;类组件&#xff09;JSX语法书写规范JSX插入的内容JSX属性绑定JSX类绑…

前端基础(CSS)——css介绍 常用样式 案例—进化到Bootstrap——进化到Element-UI(未完待续)

目录 引出CSS相关1.css写在哪里&#xff1f;2.css的选择器【重要】&#xff08;1&#xff09;标签选择器---div{}&#xff08;2&#xff09;id选择器----#div01{}&#xff08;3&#xff09;类选择器---class"div01"&#xff0c;.dav01{}&#xff08;4&#xff09;后代…

saas产品私有化(一) 缓存中间件适配

一.背景 名词解释:私有化一般指的是在对客交付过程中,客户由于自身数据敏感,成本控制等原因要求交付乙方将售卖的服务利用现有甲方的硬件设备或者云服务进行服务的部署. 面向场景:一般特制的是saas化的云服务软件提供商的对特殊客群的场景.其中saas行业中比较起步和规模比较大的…

流氓软件篡改微软EDGE浏览器主页面的那些伎俩

微软的EDGE浏览器很好用&#xff0c;但也很容易被绑架&#xff0c;在网上下载各类免费软件&#xff0c;只要你安装完&#xff0c;十有八九就给你把主页改成某某导航了。尽管打开EDGE直接进入360、毒霸、好123等链接对上网影响也不大&#xff0c;打开这些导航页面后&#xff0c;…

IMX6ULL裸机篇之I2C实验主控代码说明二

一. I2C实验 I2C实验内容&#xff1a; 学习如何使用 I.MX6U 的 I2C 接口来驱动 AP3216C&#xff0c;读取 AP3216C 的传感器数据。 I2C读写数据时序图&#xff1a; I2C写数据时序图如下&#xff1a; I2C读数据时序图如下&#xff1a; 二. I2C主控读写时序 1. 读数据与写数…

MMDetection学习记录(二)之配置文件

文件结构 config文件 在 config_base_ 文件夹下有 4 个基本组件类型&#xff0c;分别是&#xff1a;数据集(dataset)&#xff0c;模型(model)&#xff0c;训练策略(schedule)和运行时的默认设置(default runtime)。 命名风格 {model}_[model setting]_{backbone}_{neck}_[no…

微信小程序项目实例——密码管理器

今日推荐&#x1f481;‍♂️ 自疫情后武林广场的音乐喷泉再次开启⛲⛲⛲ 坐在最佳视角下观赏了一场久违的表演&#x1f386;&#x1f386;&#x1f386; &#x1f52e;&#x1f52e;&#x1f52e;&#x1f52e;&#x1f52e;往期优质项目实例&#x1f52e;&#x1f52e;&…

在三台Linux虚拟机上完成构建集群的前置准备

前言 从现在开始进入到实操阶段&#xff0c;将要在VMware软件中创建多台Linux虚拟机&#xff0c;并进行系统设置。 需要同学们拥有前置知识&#xff1a; VMware的使用经验&#xff0c;知道什么是虚拟机并在VMware中创建过Linux虚拟机&#xff08;CentOS系统&#xff09; 熟…

go语言学习——8

文章目录 文件操作打开文件文件读取写文件判读文件或文件夹是否存在拷贝文件统计文件字符命令行参数flag包解析命令行参数 文件操作 os.File封装所有文件相关操作&#xff0c;File是一个结构体 打开文件 package mainimport ("fmt""os" )func main() {file…

重载运算符三个const的作用

const Point operator(const Point &point) const{ } 尝试去理解const,然后搞懂为什么这里放置const。 const 用于修饰其后面跟着的名字&#xff0c;使其为常量&#xff0c;不可被修改。 1.第一个const 的位置后面是函数返回值类型&#xff0c;表明函数返回的是常量&#…

Yolov8涨点神器:创新卷积块NCB和创新Transformer 块NTB,助力检测,提升检测精度

🏆🏆🏆🏆🏆🏆Yolov8魔术师🏆🏆🏆🏆🏆🏆 ✨✨✨魔改网络、复现前沿论文,组合优化创新 🚀🚀🚀小目标、遮挡物、难样本性能提升 🍉🍉🍉定期更新不同数据集涨点情况 本博客将具有部署友好机制的强大卷积块和变换块,即NCB和NTB,引入到yolo…

spring源码的简单梳理之bean的初始化过程

我们都知道spring中最核心的就是容器的概念&#xff0c;而交于spring管理的对象称为bean对象。在spring中我们这次以xml配置bean的方式进行简单模拟spring创建bean的初始化过程。 1、首先我们先来一起研究一下一个xml文件中bean的结构。 我们可以看到一个bean是一个标签所扩住…

【STM32CubeMX】WS2812彩灯

前言 有时间我就按照网上的时序推理了WS2812的传输时序。之前就推过时序了&#xff0c;但是当时时序好像没对&#xff0c;因为没用逻辑分析仪查看&#xff0c;就以为通过电片机的运行主频&#xff0c;在控制NOP&#xff0c;就能得到us级的延时控制&#xff0c;但是真实的情况是…