并发编程 - 并发可见性,原子性,有序性 与 JMM内存模型

1. 并发三大特性

并发编程Bug的源头: 原子性可见性有序性 问题

1.1 原子性

一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行。 在 Java
中,对基本数据类型的变量的读取和赋值操作是原子性操作(64位处理器)。 不采取任何的原子性保障措施的自增操作并不是原子性的,比如i++操作。

原子性案例分析

下面例子模拟多线程累加操作

java 复制代码
public class AtomicTest {
    private static volatile int counter = 0;

    public static void main(String[] args) {

        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    //synchronized (AtomicTest.class) {
                        counter++;
                   // }
                }

            });
            thread.start();
        }

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //思考counter=?
        System.out.println(counter);

    }

}

执行结果不确定, 与预期结果不符合,存在线程安全问题

如何保证原子性?

1.通过 synchronized 关键字保证原子性
2.通过 Lock锁保证原子性
3.通过 CAS保证原子性
思考:在 32 位的机器上对 long 型变量进行加减操作是否存在并发隐患?
https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.7

1.2 可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到 修改的值。

可见性案例分析

下面是模拟两个线程对共享变量操作的例子,用来分析线程间的可见性问题

java 复制代码
@Slf4j
public  class VisibilityTest {

    // volatile   -> lock addl $0x0,(%rsp)
    private  boolean flag = true;
   // private volatile boolean flag = true;
    //private volatile int count;


    public  synchronized void refresh() {
        // 希望结束数据加载工作
        flag = false;
        System.out.println(Thread.currentThread().getName() + "修改flag:"+flag);
    }

    public void load() {
        System.out.println(Thread.currentThread().getName() + "开始执行.....");
        while (flag) {
            //TODO  业务逻辑:加载数据
            //shortWait(10000);
            //synchronized可以保证可见性
            //System.out.println("正在加载数据......");
           // count++;
            //添加一个内存屏障   可以保证可见性
            //UnsafeFactory.getUnsafe().storeFence();
//            try {
//                Thread.sleep(0);
//            } catch (InterruptedException e) {
//                throw new RuntimeException(e);
//            }
            //Thread.yield(); //让出cpu使用权

        }

        System.out.println(Thread.currentThread().getName() + "数据加载完成,跳出循环");
    }


    public static void main(String[] args) throws InterruptedException {
        VisibilityTest test = new VisibilityTest();


        // 线程threadA模拟数据加载场景
        Thread threadA = new Thread(() -> test.load(), "threadA");
        threadA.start();

        // 让threadA先执行一会儿后再启动线程B
        Thread.sleep(1000);

        // 线程threadB通过修改flag控制threadA的执行时间,数据加载可以结束了
        Thread threadB = new Thread(() -> test.refresh(), "threadB");
        threadB.start();

    }

当flag没有volatile修饰时,不可见,执行结果 线程A跳不出循环

运行结果:threadA没有跳出循环,也就是说threadB对共享变量flag的更新操作对threadA不可见, 存在可见性问题。

思考: 上面例子中为什么多线程对共享变量的操作存在可见性问题?

当flag有volatile修饰时,具有可见性,执行结果 线程A可以跳循环

当flag没有volatile修饰时,但是在 load()方法内的while()中输出打印语句如:System.out.println("正在加载数据......")后,,执行结果 **线程A还是可以跳循环,原因是**println()方法内有synchronized (this),具有可见性。

复制代码
当flag没有volatile修饰时,但是在load()方法内的while()中加上内存屏障后,执行结果线程A也是可以跳循环,具有可见性。
java 复制代码
public class UnsafeFactory {

    /**
     * 获取 Unsafe 对象
     * @return
     */
    public static Unsafe getUnsafe() {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            return (Unsafe) field.get(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 获取字段的内存偏移量
     * @param unsafe
     * @param clazz
     * @param fieldName
     * @return
     */
    public static long getFieldOffset(Unsafe unsafe, Class clazz, String fieldName) {
        try {
            return unsafe.objectFieldOffset(clazz.getDeclaredField(fieldName));
        } catch (NoSuchFieldException e) {
            throw new Error(e);
        }
    }


}

当flag没有volatile修饰时,但是在 load()方法内的while()中线程睡眠的方法:如 Thread.sleep(0);后**,执行结果** **线程A也是可以跳循环,**sleep(0)方法内部调用了内存屏障,具有可见性!

当sleep中的时间值为0时,相当于调用了 Thread.yield(); 让出cpu使用权

如何保证可见性

1. 通过 volatile 关键字保证可见性

2. 通过 内存屏障保证可见性

3. 通过 synchronized 关键字保证可见性
4. 通过 Lock锁保证可见性

1.3 有序性

即程序执行的顺序按照代码的先后顺序执行。 为了提升性能,编译器和处理器常常会对指令做重排 序,所以存在有序性问题。

有序性案例分析

思考:下面的Java程序中x和y的最终结果是什么?

java 复制代码
public class ReOrderTest {

    private static  int x = 0, y = 0;
    private  static  int a = 0, b = 0;

    public static void main(String[] args) throws InterruptedException {
        int i=0;
        while (true) {
            i++;
            x = 0;
            y = 0;
            a = 0;
            b = 0;

            /**
             *  x,y的值是多少:  0,1 1,0  1,1  0,0
             */
            Thread thread1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    //用于调整两个线程的执行顺序
                    shortWait(20000);
                    a = 1; //volatile 写
                    // 内存屏障StoreLoad   lock; addl $0,0(%%rsp)
                    UnsafeFactory.getUnsafe().storeFence();
                    x = b; //volatile 读

                }
            });
            Thread thread2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    b = 1;
                    y = a;

                }
            });

            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();

            System.out.println("第" + i + "次(" + x + "," + y + ")");
            if (x==0&&y==0){
                break;
            }
        }
    }

    public static void shortWait(long interval){
        long start = System.nanoTime();
        long end;
        do{
            end = System.nanoTime();
        }while(start + interval >= end);
    }

}

执行结果:x,y出现了0,0的结果,程序终止。出现这种结果有可能是重排序导致的

如何保证有序性

1.通过 volatile 关键字保证有序性
2.通过 内存屏障保证有序性
3.通过 synchronized关键字保证有序性
4.通过Lock锁保证有序性

2. Java内存模型详解

在并发编程中,需要处理的两个关键问题:

1) 多线程之间如何通信(线程之间以何种机制来交换数据)。
2)多线程之间如何同步 (控制不同线程间操作发生的相对顺序)。
线程之间常用的通信机制有两种:共享内存和消息传递,Java采用的是共享内存模型。

2.1 Java内存模型的抽象结构

Java线程之间的通信由Java内存模型( Java Memory Model,简称JMM )控制,JMM决定一个 线程对共享变量的写入何时对另一个线程可见。
从抽象的角度来看, JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内 存中,每个线程都有一个私有的本地内存,本地内存中存储了共享变量的副本。 本地内存是JMM的一 个抽象概念,并不真实存在,它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。
根据JMM的规定, 线程对共享变量的所有操作都必须在自己的本地内存中进行,不能直接从主内 存中读取

从上图看,线程A和线程B之间要通信的话,必须经历以下两个步骤:
1)线程A把本地内存A中更新过的共享变量刷新到主内存中
2)线程B到主内存中去读取线程A之前已更新过的共享变量
所以,线程A无法直接访问线程B的工作内存,线程间通信必须经过主内存。 JMM通过控制主内存与每 个线程的本地内存之间的交互,来为Java程序提供内存可见性的保证。

主内存与工作内存交互协议

关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工
作内存同步到主内存之间的实现细节, Java内存模型定义了以下八种 原子操作 来完成 :
lock(锁定): 作用于 主内存的变量 ,把一个变量标识为一条线程独占状态。
unlock(解锁): 作用于 主内存变量, 把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁 定。
read(读取): 作用于 主内存变量, 把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
load(载入): 作用于 工作内存的变量 ,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
use(使用): 作用于 工作内存的变量 ,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要 使用变量的值的字节码指令时将会执行这个操作。
assign(赋值): 作用于 工作内存的变量 ,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机 遇到一个给变量赋值的字节码指令时执行这个操作。
store(存储): 作用于 工作内存的变量, 把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
write(写入): 作用于 主内存的变量 **,**它把store操作从工作内存中得到的变量的值放入主内存的变量中。

Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:
如果要把一个变量从主内存中复制到工作内存,就需要按顺序地执行read和load操作, 如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。但 Java内存模型只要求上述操作必须按顺序执行,而 没有保证必须是连续执行。
不允许read和load、store和write操作之一单独出现。
不允许一个线程丢弃它的最近assign的操作,即 变量在工作内存中改变了之后必须同步到主内存中。
不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行 lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现
如果对一个变量执行lock操作,将会清空工作内存中此变量的值 ,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)

可见性案例深入分析

重点:结合可见性案例理解主内存和工作内存的交互过程

相关推荐
ClouGence29 分钟前
CloudCanal + Paimon + SelectDB 从 0 到 1 构建实时湖仓
数据库
渣哥34 分钟前
原来 Java 里线程安全集合有这么多种
java
间彧41 分钟前
Spring Boot集成Spring Security完整指南
java
间彧1 小时前
Spring Secutiy基本原理及工作流程
java
Java水解2 小时前
JAVA经典面试题附答案(持续更新版)
java·后端·面试
洛小豆4 小时前
在Java中,Integer.parseInt和Integer.valueOf有什么区别
java·后端·面试
前端小张同学5 小时前
服务器上如何搭建jenkins 服务CI/CD😎😎
java·后端
ytadpole5 小时前
Spring Cloud Gateway:一次不规范 URL 引发的路由转发404问题排查
java·后端
华仔啊5 小时前
基于 RuoYi-Vue 轻松实现单用户登录功能,亲测有效
java·vue.js·后端