Java并发模型


观看《Java并发编程的艺术》所做笔记

Java内存模型

并发编程模型分为: 共享内存并发模型,消息传递并发模型

Java是共享内存并发模型,线程间的通信是隐式的

局部变量,方法参数,异常处理器参数不属于线程间的共享变量,不受内存模型影响

目的: 为了定义程序中各种共享变量访问规则

Java内存模型规定:

  1. 所有的共享变量都存储在主内存中(物理上是虚拟机的一部分)
  2. 每条线程有自己的工作内存
  3. 线程的工作内存保存了被该线程使用变量的主内存副本
  4. 线程对内存的所有操作(读写等)都要在工作内存进行,不能直接操作主内存
  5. 不同线程间无法访问对方工作内存的变量,线程间变量值传递需要通过主内存来完成

注意: 主内存与工作内存 可以类比为 内存与高速缓冲存储器(cache)

从主内存中读取数据到工作内存,线程操作工作内存修改数据,最终从工作内存写到主内存上

从内存中读取数据到cache,CPU操作cache上的数据,最终从cache再写回到主内存上

内存模型

内存模型:

  1. 处理器内存模型
  2. 语言级内存模型(JMM)
  3. 顺序一致性内存模型

程序员希望程序按照自己写的顺序来执行

编译器,处理器希望对它们重排序的约束经量的低

这正好相反

内存模型设计的越弱,对编译器,处理器束缚越少,它们就可以用重排序来提高性能

所以模型3->1的性能逐渐增加,易编程性逐渐降低

JMM保证内存可见性

  1. 保证单线程中执行结果与顺序一致性内存模型执行结果相同
  2. 保证正确同步的多线程程序与顺序一致性内存模型执行结果相同
  3. 保证未同步/未正确同步的多线程程序有最小安全保障:读到的值要么是之前线程写的值要么是默认值

交互操作以及注意事项

/**
 * @author Tc.l
 * @Date 2020/11/3
 * @Description:
 * 线程A读到num为0,让num为0时,线程A就循环
 * 然后通过主线程修改num的值
 * 但是程序不能停下来 一直处于运行状态(线程A依旧在循环)
 */
public class JavaMemoryModel {
    static int num = 0;
    public static void main(String[] args) {
        new Thread(()->{
            while (num==0){
                
            }
        },"线程A").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        num=1;
    }
}

上面的代码用Java内存模型图可以这样表示:

在这里插入图片描述

8种内存交互操作

主内存与工作内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)

  1. lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态

  2. unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定

  3. read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用

  4. load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中

  5. use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令

  6. assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中

  7. store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用

  8. write  (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中

使用规则与注意事项

JMM对这八种指令的使用,制定了如下规则:

  1. 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write

  2. 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存

  3. 不允许一个线程将没有assign的数据从工作内存同步回主内存,即未改变数据,又把数据从工作内存写回主内存

  4. 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过load,assign操作

  5. 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁

  6. 如果对一个变量进行lock操作,加锁会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值

  7. 如果一个变量没有被lock,就不能对其进行unlock操作也不能unlock一个被其他线程锁住的变量

  8. 对一个变量进行unlock操作之前,必须把此变量同步回主内存

重排序

为了提高性能,编译器和处理器会进行指令重排序

JMM通过编译器禁止特定类型编译器重排序和内存屏障来禁止特定类型的处理器来提供一致的内存可见性

public class Reorder {
    static int a, b, x, y;
    public static void main(String[] args){

        long count = 0;
        while (true) {
            count++;
            a = 0;b = 0;x = 0;y = 0;
            Thread thread1 = new Thread(() -> {
                a = 1;
                x = b;
            });
            Thread thread2 = new Thread(() -> {
                b = 1;
                y = a;
            });
            thread1.start();
            thread2.start();
            try {
                thread1.join();
                thread2.join();
            } catch (Exception e) {}
            if (x == 0 && y == 0) {
                break;
            }
        }
        System.out.println("count=" + count + ",x=" + x + ",y=" + y);
    }
}

在这里插入图片描述

数据依赖性
  1. 写后读
  2. 写后写
  3. 读后写

单线程中存在数据依赖性,编译器和处理器不会做重排序

如果单线程中不存在数据依赖性,编译器和处理器可能会做重排序
(如果它在单线程中不存在数据依赖性但是在多线程中存在就有几率重排序,打乱我们想要的效果)

线程A
a = 1;//1
x = b;//2

线程B:
b = 2;//3
y = a;//4

可能得到 x=0 ,y=0的结果

因为操作1和2 ,操作3和4 不存在依赖性, 可能对操作1和2(3和4)重排序,导致线程A读到的b是还没有赋值的b(线程B读到的a是还没有赋值的a),因此x = 0,y = 0

重排序规则

volatile的重排序规则

volatile通过在指令中插入内存屏障,把修改后的值刷新到主内存,失效的值需要重新去主内存中读取,来保证可见性

保证新值能立即同步到主内存,以及每次使用前立即从主内存刷新

锁的重排序规则

当线程释放锁前,要把线程本地内存中的共享变量数据更新到主内存中

当另一个线程获得锁时,对应的本地内存共享变量数据会失效,需要去主内存读取共享变量

对变量执行unlock前,要先执行store,write操作写入主内存

  • 公平锁

    • 获取锁时: 先读取volatile state变量
    • 释放锁时: 修改volatile state变量
  • 非公平锁

    • 获取锁时: 先读取volatile state变量
    • 释放锁时: CAS操作更新volatile state变量
  • volatile变量读写和CAS操作可以实现线程间的通信,这成为了JUC的基石

    实现模式

    1. 声明共享变量为volatile
    2. 使用CAS操作来实现线程间的同步
    3. volatile变量 + CAS操作 实现线程间的通信

final域的重排序规则

  • 写: 禁止把final域的写重排到构造器外,保证线程可见前已经完成对final域的初始化(需要保证final域不会溢出)

    • 溢出: 在构造器返回前,被其他线程看到这个final引用域
  • 读: 读final域前,必须先读这个final域的对象引用

  • 总结: 被final修饰不能改变所以无须同步可以被其他线程正确访问(引用未逃逸的情况下)

先行发生原则

happens-before

  • 程序次序规则: 同个线程内,书写在前面的操作先行发生于书写在后面的操作
  • 管程锁定规则: 一个unlock操作先行发生在后面对同一个锁的lock操作
  • volatile变量规则: 对一个volatile变量的写操作先行发生于后面对这个变量的读操作
  • 线程启动规则: 线程start()先行发生于此线程每个动作
  • 线程终止规则: 线程所有操作先行发生于此线程终止检测
  • 线程中断规则: 线程interrupt()方法调用先行发生于被中断线程的代码检测到中断事件发生
  • 对象终结规则: 一个对象初始化完成先行发生于他的finalize()方法
  • 线程join规则: 如果在线程A中执行 ThreadB.join()成功的话,线程B的操作先行发生于线程A的ThreadB.join()返回
  • 传递性: 操作A先行发生于操作B,操作B先行发生于操作C,可得操作A先行发生于操作C

时间先后顺序于先行发生原则没有因果关系,衡量并发问题不要受时间顺序干扰,一切必须以先行发生原则为准

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值