剑指JUC原理-8.Java内存模型(上)

简介: 剑指JUC原理-8.Java内存模型

Java 内存模型


JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。


JMM 体现在以下几个方面


  • 原子性 - 保证指令不会受到线程上下文切换的影响
  • 可见性 - 保证指令不会受 cpu 缓存的影响
  • 有序性 - 保证指令不会受 cpu 指令并行优化的影响


可见性


退不出的循环


先来看一个现象,main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止:

    static boolean run = true;
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            while(run){
                // ....
            }
        });
        t.start();
        sleep(1);
        run = false; // 线程t不会如预想的停下来
    }

为什么呢?分析一下:


初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存。

因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率

1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值


解决方法


volatile(易变关键字)


它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存


可见性 vs 原子性


前面例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对 volatile 变量的修改对另一个线程可见, 不能保证原子性,仅用在一个写线程,多个读线程的情况: 上例从字节码理解是这样的:

getstatic run // 线程 t 获取 run true 
getstatic run // 线程 t 获取 run true 
getstatic run // 线程 t 获取 run true 
getstatic run // 线程 t 获取 run true 
putstatic run // 线程 main 修改 run 为 false, 仅此一次
getstatic run // 线程 t 获取 run false 

比较一下之前我们将线程安全时举的例子:两个线程一个 i++ 一个 i-- ,只能保证看到最新值,不能解决指令交错

// 假设i的初始值为0 
getstatic i // 线程2-获取静态变量i的值 线程内i=0 
getstatic i // 线程1-获取静态变量i的值 线程内i=0 
iconst_1 // 线程1-准备常量1 
iadd // 线程1-自增 线程内i=1 
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1 
iconst_1 // 线程2-准备常量1 
isub // 线程2-自减 线程内i=-1 
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1

注意:


  • synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是synchronized 是属于重量级操作,性能相对更低。


synchronized 关键字可以确保多线程环境下的可见性,主要通过两个方面来实现:


互斥访问:当一个线程获取到某个对象的锁时,其他线程无法同时获取该对象的锁,只能等待锁释放。这样可以保证在同步块中对共享变量的修改操作是原子的,不会被其他线程中断。


内存可见性:当一个线程释放锁时,会将对共享变量的修改刷新到主内存中,而当另一个线程获取锁时,会从主内存中重新读取共享变量的值,确保看到最新的值。


以下是一个例子来说明 synchronized 关键字如何保证可见性:

public class SynchronizedExample {
    private static boolean flag = false;
  public static void main(String[] args) throws InterruptedException {
    Thread thread1 = new Thread(() -> {
        synchronized (SynchronizedExample.class) {
            // 修改共享变量的值
            flag = true;
            System.out.println("Thread 1: flag is set to true");
        }
    });
    Thread thread2 = new Thread(() -> {
        // 暂停一段时间,确保 thread1 先执行
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (SynchronizedExample.class) {
            // 在同步块中访问共享变量
            if (flag) {
                System.out.println("Thread 2: flag is true");
            } else {
                System.out.println("Thread 2: flag is false");
            }
        }
    });
    thread1.start();
    thread2.start();
    thread1.join();
    thread2.join();
}
}

在这个例子中,有两个线程 thread1 和 thread2,它们同时访问了共享变量 flag。首先,thread1 获取了 SynchronizedExample.class 对象的锁,并将 flag 设置为 true,然后释放锁。接着,thread2 获取了同一个锁,并在同步块中访问 flag 的值。由于 thread2 获取到了锁并读取了 flag 的最新值,因此能正确地判断出 flag 的状态。


通过 synchronized 关键字的互斥性和内存可见性的特性,确保了多线程环境下的共享变量操作的一致性和可见性。


  • 如果在前面示例的死循环中加入 System.out.println() 会发现即使不加 volatile 修饰符,线程 t 也能正确看到对 run 变量的修改了,想一想为什么?
  static boolean run = true;
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            while(run){
                // ....
                System.out.println();
            }
        });
        t.start();
        sleep(1);
        run = false; // 线程t不会如预想的停下来
    }

通过分析 System.out.println() 源码来得出结果。

synchronized 是 Java 中用于实现同步的关键字,它可以用于修饰方法和代码块。当一个线程获取了对象的锁,执行 synchronized 修饰的代码时,会将变量从主内存中拷贝到线程的本地内存中。


在 synchronized 块执行结束后,JVM 会把该线程对应的本地内存中修改过的变量刷新回主内存中,以保证不同线程间所共享的变量值的一致性。


模式之两阶段终止


// 停止标记用 volatile 是为了保证该变量在多个线程之间的可见性
// 我们的例子中,即主线程把它修改为 true 对 t1 线程可见
class TPTVolatile {
    private Thread thread;
    private volatile boolean stop = false;
    public void start(){
        thread = new Thread(() -> {
            while(true) {
                Thread current = Thread.currentThread();
                if(stop) {
                    log.debug("料理后事");
                    break;
                }
                try {
                    Thread.sleep(1000);
                    log.debug("将结果保存");
                } catch (InterruptedException e) {
                }
                // 执行监控操作
            }
        },"监控线程");
        thread.start();
    }
    public void stop() {
        stop = true;
        thread.interrupt(); // 如果设置为ture后,线程还在sleep状态,那么使用打断即可
    }
}

调用

TPTVolatile t = new TPTVolatile();
t.start();
Thread.sleep(3500);
log.debug("stop");
t.stop();

结果

11:54:52.003 c.TPTVolatile [监控线程] - 将结果保存
11:54:53.006 c.TPTVolatile [监控线程] - 将结果保存
11:54:54.007 c.TPTVolatile [监控线程] - 将结果保存
11:54:54.502 c.TestTwoPhaseTermination [main] - stop 
11:54:54.502 c.TPTVolatile [监控线程] - 料理后事


同步模式之 Balking


定义


Balking (犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做了,直接结束返回


实现


class TwoPhaseTermination {
    // 监控线程
    private Thread monitorThread;
    // 停止标记
    private volatile boolean stop = false;
    // 判断是否执行过 start 方法
    private boolean starting = false;
    // 启动监控线程
    public void start() {
        synchronized (this) {
            if (starting) { // false
                return;
            }
            starting = true;
            monitorThread = new Thread(() -> {
            while (true) {
                Thread current = Thread.currentThread();
                // 是否被打断
                if (stop) {
                    log.debug("料理后事");
                    break;
                }
                try {
                    Thread.sleep(1000);
                    log.debug("执行监控记录");
                } catch (InterruptedException e) {
                }
            }
        }, "monitor");
        monitorThread.start();
        }
    }
    // 停止监控线程
    public void stop() {
        stop = true;
        monitorThread.interrupt();
    }
}
public static void main(String[] args) throws InterruptedException {
        TwoPhaseTermination tpt = new TwoPhaseTermination();
        tpt.start();
        tpt.start();
        tpt.start();
        /*Thread.sleep(3500);
        log.debug("停止监控");
        tpt.stop();*/
    }

当前端页面多次点击按钮调用 start 时


输出

[http-nio-8080-exec-1] cn.itcast.monitor.service.MonitorService - 该监控线程已启动?(false)
[http-nio-8080-exec-1] cn.itcast.monitor.service.MonitorService - 监控线程已启动...
[http-nio-8080-exec-2] cn.itcast.monitor.service.MonitorService - 该监控线程已启动?(true)
[http-nio-8080-exec-3] cn.itcast.monitor.service.MonitorService - 该监控线程已启动?(true)
[http-nio-8080-exec-4] cn.itcast.monitor.service.MonitorService - 该监控线程已启动?(true)

一个优化就是 尽可能的让synchronized代码块中的代码较少,所以可以将不关键的因素抽取出来

public void start() {
        synchronized (this) {
            if (starting) { // false
                return;
            }
            starting = true;
        }
    monitorThread = new Thread(() -> {
            while (true) {
                Thread current = Thread.currentThread();
                // 是否被打断
                if (stop) {
                    log.debug("料理后事");
                    break;
                }
                try {
                    Thread.sleep(1000);
                    log.debug("执行监控记录");
                } catch (InterruptedException e) {
                }
            }
        }, "monitor");
        monitorThread.start();
    }

它还经常用来实现线程安全的单例

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;
    }
}


有序性


JVM 会在不影响正确性的前提下,可以调整语句的执行顺序,思考下面一段代码

static int i;
static int j;
// 在某个线程内执行如下赋值操作
i = ...; 
j = ...; 

可以看到,至于是先执行 i 还是 先执行 j ,对最终的结果不会产生影响。所以,上面代码真正执行时,既可以是

i = ...; 
j = ...;

也可以是

j = ...;
i = ...;

这种特性称之为『指令重排』,多线程下『指令重排』会影响正确性。为什么要有重排指令这项优化呢?从 CPU执行指令的原理来理解一下吧


指令级并行原理


名词


Clock Cycle Time


主频的概念大家接触的比较多,而 CPU 的 Clock Cycle Time(时钟周期时间),等于主频的倒数,意思是 CPU 能够识别的最小时间单位,比如说 4G 主频的 CPU 的 Clock Cycle Time 就是 0.25 ns,作为对比,我们墙上挂钟的Cycle Time 是 1s


例如,运行一条加法指令一般需要一个时钟周期时间


CPI


有的指令需要更多的时钟周期时间,所以引出了 CPI (Cycles Per Instruction)指令平均时钟周期数


IPC


IPC(Instruction Per Clock Cycle) 即 CPI 的倒数,表示每个时钟周期能够运行的指令数


剑指JUC原理-8.Java内存模型(中):https://developer.aliyun.com/article/1413627

目录
相关文章
|
3天前
|
存储 Java
深入理解Java虚拟机:JVM内存模型
【4月更文挑战第30天】本文将详细解析Java虚拟机(JVM)的内存模型,包括堆、栈、方法区等部分,并探讨它们在Java程序运行过程中的作用。通过对JVM内存模型的深入理解,可以帮助我们更好地编写高效的Java代码,避免内存溢出等问题。
|
5天前
|
算法 Java Go
Go vs Java:内存管理与垃圾回收机制对比
对比了Go和Java的内存管理与垃圾回收机制。Java依赖JVM自动管理内存,使用堆栈内存并采用多种垃圾回收算法,如标记-清除和分代收集。Go则提供更多的手动控制,内存分配与释放由分配器和垃圾回收器协同完成,使用三色标记算法并发回收。示例展示了Java中对象自动创建和销毁,而Go中开发者需注意内存泄漏。选择语言应根据项目需求和技术栈来决定。
|
2天前
|
缓存 算法 内存技术
深入理解操作系统内存管理:原理与实践
【4月更文挑战第30天】本文旨在深入探讨操作系统中的内存管理机制,包括物理内存的分配与回收、虚拟内存技术、分页系统以及内存优化策略。通过对内存管理概念的详细解读和实际案例分析,读者将获得对操作系统如何处理内存资源的全面认识,并了解如何在实践中应用这些知识以提高系统性能和稳定性。
|
3天前
|
存储 机器学习/深度学习 Java
【Java探索之旅】数组使用 初探JVM内存布局
【Java探索之旅】数组使用 初探JVM内存布局
12 0
|
3天前
|
存储 算法 安全
深入理解操作系统内存管理:原理与实践
【4月更文挑战第29天】 在现代计算机系统中,操作系统的内存管理是其核心功能之一。有效的内存管理不仅关乎系统性能,也直接影响到用户程序的稳定性和安全性。本文将详细探讨操作系统内存管理的基本原理、关键技术以及当前的挑战和创新方向。通过对页式管理、段式管理和段页式管理等技术的深入分析,我们旨在为读者提供一个清晰、系统的内存管理知识框架,并讨论虚拟内存技术如何帮助解决物理内存不足的问题。同时,考虑到安全性日益成为关注焦点,文中还将介绍内存保护机制和内存隔离技术。最后,结合最新的硬件发展趋势,如非易失性内存(NVM)的出现,本文也将对内存管理的未来发展方向进行展望。
|
3天前
|
存储 安全 Java
【Java EE】CAS原理和实现以及JUC中常见的类的使用
【Java EE】CAS原理和实现以及JUC中常见的类的使用
|
4天前
|
存储 缓存
深入理解操作系统:虚拟内存管理的原理与实践
【4月更文挑战第28天】 在现代计算机系统中,虚拟内存是操作系统提供的一项重要功能,它允许程序使用比物理内存更大的地址空间。本文将深入探讨虚拟内存的工作原理,包括分页、分段和请求分页等技术,以及它们在操作系统中的应用。
|
4天前
|
安全 Java 编译器
Java面向对象思想以及原理以及内存图解(下)
Java面向对象思想以及原理以及内存图解(下)
13 0
|
4天前
|
Java
Java面向对象思想以及原理以及内存图解(上)
Java面向对象思想以及原理以及内存图解
15 0
|
5天前
|
存储 算法 安全
深入理解操作系统内存管理:原理与实践
【4月更文挑战第27天】 在现代计算机系统中,操作系统的内存管理是其核心功能之一,它负责协调和分配系统内存资源,确保程序高效稳定地运行。本文将详细探讨操作系统内存管理的基本原理、关键技术以及面临的挑战。通过对内存分配策略、分页机制、虚拟内存技术等关键概念的剖析,揭示操作系统如何优化内存使用,支持多任务并发执行,并保证系统的稳定与安全。同时,文中还将对最新的内存管理技术和趋势进行展望,为读者提供一个全面、深入的理解框架。