Java 内存模型

Java 内存模型

1、JMM:Java Memory Model

2、定义主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等

3、JMM 体现

(1)原子性:保证指令不会受到线程上下文切换的影响

(2)可见性:保证指令不会受 CPU 缓存的影响

(3)有序性:保证指令不会受 CPU 指令并行优化的影响

 

可见性

1、volatile

(1)修饰成员变量、静态成员变量

(2)作用:避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存

(3)保证可见性:在多个线程之间,一个线程对 volatile 变量的修改,对另一个线程可见

(4)不能保证原子性,仅用在一个写线程,多个读线程的情况

2、synchronized

(1)既可以保证代码块的原子性,也同时保证代码块内变量的可见性

(2)缺点:属于重量级操作,性能相对更低

 

两阶段终止模式

1、使用 isInterrupted、interrupt 实现

(1)缺点:可能出现 InterruptedException,打断标记容易遗漏、出错

(2)使用 volatile 改进

2、volatile 实现示例

(1)缺点:在 start() 中,没有限制创建监控线程,因为监控线程只需一个

class Volatile {
    
    //监控线程
    private Thread thread;
    
    //停止标记
    private volatile boolean stop = false;
    
    //启动监控线程
    public void start(){
        thread = new Thread(() -> {
            while(true) {
                Thread current = Thread.currentThread();
                if(stop) {
                    //线程被打断,则执行退出的前置操作
                    break;
                }
                try {
                    //监控间隔
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                }
                // 执行监控操作
            }
        },"monitor");
        thread.start();
    }
    
    //停止监控线程
    public void stop() {
        stop = true;
        //若在sleep期间,stop设置为true,该方法可以在避免等待剩余sleep时间
        thread.interrupt();
    }
}

 

同步模式:Balking

1、Balking(犹豫)模式使用在一个线程,发现另一个线程或本线程,已经执行某一件相同的事,则本线程就无需再执行,直接结束返回

2、改进两阶段终止模式

class Volatile {

    //监控线程
    private Thread thread;

    //停止标记
    private volatile boolean stop = false;

    //表示是否有线程在执行启动
    private volatile boolean starting = false;

    //启动监控线程
    public void start(){

        //尝试启动监控线程,synchronized避免多线程进入
        synchronized (this) {
            if (starting) {
                return;
            }
            starting = true;
        }

        thread = new Thread(() -> {
            while(true) {
                Thread current = Thread.currentThread();
                if(stop) {
                    //线程被打断,则执行退出的前置操作
                    break;
                }
                try {
                    //监控间隔
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                }
                // 执行监控操作
            }
        },"monitor");
        thread.start();
    }

    //停止监控线程
    public void stop() {
        stop = true;
        starting = false;
        //若在sleep期间,stop设置为true,该方法可以在避免等待剩余sleep时间
        thread.interrupt();
    }
}

3、应用:实现线程安全的单例

public final class Singleton {
    
    private Singleton() {
    }
    
    private static Singleton INSTANCE = null;
    
    public static synchronized Singleton getInstance() {
        if (INSTANCE != null) {
            return INSTANCE;
        }
        INSTANCE = new Singleton();
        return INSTANCE;
    }
}

 

指令级并行概念

1、Clock Cycle Time

(1)时钟周期时间

(2)等于主频的倒数,CPU 能够识别的最小时间单位

(3)如:4G 主频的 CPU 的 Clock Cycle Time 为 0.25 ns

2、CPI

(1)Cycles Per Instruction

(2)有的指令需要更多的时钟周期时间,所以引出指令平均时钟周期数

3、IPC

(1)Instruction Per Clock Cycle, 即 CPI 的倒数

(2)表示每个时钟周期能够运行的指令数

4、程序的 CPU 执行时间

(1)user + system 时间

(2)程序 CPU 执行时间 = 指令数 * CPI * Clock Cycle Time 

 

指令重排序优化

1、现代 CPU 会设计为一个时钟周期,完成一条执行时间最长的 CPU 指令

2、指令可再划分成更小的阶段,如:取指令 -> 指令译码 -> 执行指令 -> 内存访问 -> 数据写回

(1)instruction fetch (IF)

(2)instruction decode (ID)

(3)execute (EX)

(4)memory access (MEM)

(5)register write back (WB) 

3、在不改变程序结果的前提下,这些指令的各个阶段,可以通过重排序、组合,来实现指令级并行

4、现代 CPU 支持多级指令流水线

(1)如:支持同时执行:取指令 -> 指令译码 -> 执行指令 -> 内存访问 -> 数据写回,为五级指令流水线

(2)CPU 可以在一个时钟周期内,同时运行五条指令的不同阶段(相当于一条执行时间最长的复杂指令),IPC = 1

(3)本质上,流水线技术并不能缩短单条指令的执行时间,但提高指令的吞吐率

5、SuperScalar 处理器

(1)大多数处理器包含多个执行单元,并不是所有计算功能都集中在一起,可以再细分为整数运算单元、浮点数运算单元等

(2)可以把多条指令做到并行获取、译码等

(3)CPU 可以在一个时钟周期内,执行多于一条指令,IPC > 1

6、禁止指令重排:volatile

 

volatile

1、底层实现原理是内存屏障,Memory Barrier(Memory Fence)

(1)对 volatile 变量的写指令后会加入写屏障

(2)对 volatile 变量的读指令前会加入读屏障

2、保证可见性

(1)写屏障(sfence)保证在该屏障之前,对共享变量的改动,都同步到主存当中

(2)读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据

3、保证有序性

(1)写屏障确保指令重排序时,不会将写屏障之前的代码,排在写屏障之后

(2)读屏障确保指令重排序时,不会将读屏障之后的代码,排在读屏障之前

(3)注意:volatile 不能解决指令交错

(4)写屏障只保证之后的读,能够读到最新的结果,但不能保证其它线程的读在本线程前

(5)有序性的保证只保证本线程内相关代码不被重排序

4、更底层是读写变量时,使用 lock 指令,保证多核 CPU 之间的可见性、有序性

5、synchronized 不能禁止代码块内的指令重排

(1)但可以保证其有序性

(2)原因:synchronized 保证单线程执行代码块,不会出现多线程下的指令重排问题

(3)即有序性建立在原子性基础上

 

happens-before

1、规定哪些写操作对其它线程的读操作可见,是可见性与有序性的一套规则总结

2、抛开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见

3、以下变量都是指成员变量 / 静态成员变量

(1)t1 线程释放锁对象之前,对变量的写,对于接下来对 m 加锁的 t2 线程,对该变量的读可见

static int x;

static Object m = new Object();

new Thread(()->{
    synchronized(m) {
        x = 10;
    }
},"t1").start();

new Thread(()->{
    synchronized(m) {
        System.out.println(x);
    }
},"t2").start();

(2)t1 线程对 volatile 变量的写,对接下来 t2 线程,对该变量的读可见

volatile static int x;

new Thread(()->{
    x = 10;
},"t1").start();

new Thread(()->{
    System.out.println(x);
},"t2").start();

(3)t1 线程 start 前,对变量的写,对 t1 线程开始后,对该变量的读可见

static int x;

x = 10;

new Thread(()->{
    System.out.println(x);
},"t1").start();

(4)t1 线程结束前,对变量的写,对主线程得知 t1 结束后的读可见(如:主线程调用 t1 的 isAlive() 或 join(),等待它结束)

static int x;

Thread t1 = new Thread(()->{
    x = 10;
},"t1");

t1.start();

t1.join();

System.out.println(x);

(5)线程 t1 打断(interrupt)t2 线程前,对变量的写,对于 t3 线程得知 t2 被打断后,对变量的读可见(通过 t2.interrupted 或 t2.isInterrupted) 

static int x;

public static void main(String[] args) {
    
    Thread t2 = new Thread(()->{
        while(true) {
            if(Thread.currentThread().isInterrupted()) {
                System.out.println(x);
                break;
            }
        }
    },"t2");
    
    t2.start();
    
    new Thread(()->{
        sleep(1);
        x = 10;
        t2.interrupt();
    },"t1").start();
    
    while(!t2.isInterrupted()) {
        Thread.yield();
    }
    
    System.out.println(x);
}

(6)对变量默认值(0,false,null)的写,对其它线程对该变量的读可见

volatile static int x;

static int y;

new Thread(()->{ 
    y = 10;
    x = 20;
},"t1").start();

new Thread(()->{
    System.out.println(x);
    System.out.println(y);
},"t2").start();

(7)具有传递性,如果 x happens-before -> y 并且 y happens-before -> z,那么有 x happens-before -> z

 

 单例模式的线程安全

1、饿汉式(静态常量)

//final,防止被继承,子类方法破坏单例
public final class Singleton implements Serializable {
    
    //private,防止直接创建,破坏单例
    //不能防止反射创建新的实例
    private Singleton() {}

    //静态常量,JVM在类加载阶段,保证其创建时的线程安全
    private static final Singleton INSTANCE = new Singleton();
    
    /*
    提供静态方法,而不是直接将 INSTANCE 设置为 public
    1、更好的封装性
    2、可以改进为懒汉式
    3、对单例更好地控制
    4、支持泛型
    */
    public static Singleton getInstance() {
        return INSTANCE;
    }

    //防止反序列化破坏单例
    public Object readResolve() {
        return INSTANCE;
    }
}

2、枚举类

(1)枚举单例,底层通过静态常量,限制实例个数

(2)枚举单例为静态常量,JVM 保证在创建时的线程安全

(3)枚举单例不能被反射破坏单例

(4)枚举单例不能被反序列化破坏单例

(5)枚举单例属于饿汉式

(6)枚举单例可以加入构造方法,实现在创建时的初始化逻辑

enum Singleton { 
    INSTANCE; 
}

3、懒汉式(静态内部类)

(1)类初始化是懒加载的

(2)初始化时机:首次访问类的静态变量 / 静态方法时

(3)使用 LazyHolder,才加载

(4)static final 修饰的引用类型不会在准备阶段赋值,而是在初始化阶段赋值

(5)类的初始化方法:<clinit>()

(6)虚拟机会保证一个类的 <clinit>() 在多线程环境中被正确地加锁、同步

(7)如果多个线程同时去初始化一个类,则只会有一个线程去执行这个类的 <clinit>(),其他线程都需要阻塞等待,直到活动线程执行 <clinit>() 完毕

(8)如果之前的线程成功加载了类,则等在队列中的线程,就没有机会再执行 <clinit>(),当需要使用这个类时,虚拟机会直接返回给它已经准备好的信息

public final class Singleton {
    private Singleton() { }
    // 问题1:属于懒汉式还是饿汉式
    private static class LazyHolder {
        static final Singleton INSTANCE = new Singleton();
    }
    // 问题2:在创建时是否有并发问题
    public static Singleton getInstance() {
        return LazyHolder.INSTANCE;
    }
}
posted @   半条咸鱼  阅读(33)  评论(0编辑  收藏  举报
(评论功能已被禁用)
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示