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

您所在的位置:网站首页 java多线程有序性 java并发编程:可见性、原子性、有序性三大特性详解

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

2024-07-05 21:43:52| 来源: 网络整理| 查看: 265

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

可见性

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

导致可见性的原因 线程交叉执行

线程交叉执行多数情况是由于线程切换导致的,例如下图中的线程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 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 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 @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关键字原子性可以保障无法保障可以保障可以保障可见性可以保障可以保障可以保障可以保障有序性无法保障一定程度保障可以保障可以保障


【本文地址】

公司简介

联系我们

今日新闻


点击排行

实验室常用的仪器、试剂和
说到实验室常用到的东西,主要就分为仪器、试剂和耗
不用再找了,全球10大实验
01、赛默飞世尔科技(热电)Thermo Fisher Scientif
三代水柜的量产巅峰T-72坦
作者:寞寒最近,西边闹腾挺大,本来小寞以为忙完这
通风柜跟实验室通风系统有
说到通风柜跟实验室通风,不少人都纠结二者到底是不
集消毒杀菌、烘干收纳为一
厨房是家里细菌较多的地方,潮湿的环境、没有完全密
实验室设备之全钢实验台如
全钢实验台是实验室家具中较为重要的家具之一,很多

推荐新闻


图片新闻

实验室药品柜的特性有哪些
实验室药品柜是实验室家具的重要组成部分之一,主要
小学科学实验中有哪些教学
计算机 计算器 一般 打孔器 打气筒 仪器车 显微镜
实验室各种仪器原理动图讲
1.紫外分光光谱UV分析原理:吸收紫外光能量,引起分
高中化学常见仪器及实验装
1、可加热仪器:2、计量仪器:(1)仪器A的名称:量
微生物操作主要设备和器具
今天盘点一下微生物操作主要设备和器具,别嫌我啰嗦
浅谈通风柜使用基本常识
 众所周知,通风柜功能中最主要的就是排气功能。在

专题文章

    CopyRight 2018-2019 实验室设备网 版权所有 win10的实时保护怎么永久关闭