如何制作一个网站前的小图标以展示青海省建筑信息平台?

摘要:网站前面的小图标怎么做,青海省建筑信息平台,wordpress两种语言主题,宜昌网站设计公司一、JVM 内存模型概述 (1) 为什么会出现 JVM 内存模型呢? JVM 内存模型是为规范描述 Java 虚拟机在
网站前面的小图标怎么做,青海省建筑信息平台,wordpress两种语言主题,宜昌网站设计公司一、JVM 内存模型概述 (1) 为什么会出现 JVM 内存模型呢#xff1f; JVM 内存模型是为规范描述 Java 虚拟机在执行 Java 程序时#xff0c;将程序中的数据和代码存储到计算机内存中的方式和规则。JVM 内存模型定义 Java 虚拟机所使用的内存结构以及内存区域之间的关系…一、JVM 内存模型概述 (1) 为什么会出现 JVM 内存模型呢 JVM 内存模型是为规范描述 Java 虚拟机在执行 Java 程序时将程序中的数据和代码存储到计算机内存中的方式和规则。JVM 内存模型定义 Java 虚拟机所使用的内存结构以及内存区域之间的关系使得 Java 程序能够更高效地运行。 Java 虚拟机是一种基于栈式架构的虚拟机不同于物理机器上的基于寄存器的架构。因此Java 虚拟机需要一个内存模型来处理 Java 程序所涉及的数据和代码存储以及执行期间的内存管理。JVM 内存模型的出现使得 Java程序能够更加规范、高效、灵活地运行。 JVM 内存模型图如下所示 程序计数器Program Counter Register每个线程都有一个独立的程序计数器用于存储线程当前执行的字节码指令的地址。Java 虚拟机栈JVM Stack每个线程都有一个独立的Java虚拟机栈用于存储局部变量、操作数栈、返回值等信息。本地方法栈Native Method Stack与Java虚拟机栈类似但用于执行本地方法。堆Heap用于存储Java对象实例是所有线程共享的。方法区Method Area用于存储类的结构信息、静态变量、常量池等信息是所有线程共享的。运行时常量池Runtime Constant Pool方法区的一部分用于存储编译时生成的各种字面量和符号引用。直接内存Direct Memory一种使用非JVM管理的内存但通过JVM的API使用它它通常用于提高I/O操作的性能。 (2) JVM 内存为什么要这样划分主要有以下几点 分离程序数据和 JVM 内部数据结构Java虚拟机需要存储很多内部结构和状态信息同时还需要存储Java程序的数据和代码。为了让这些不同类型的数据结构能够互相独立Java虚拟机将其存储在不同的内存区域中。 内存管理需要JVM 需要对内存进行管理和优化。划分不同的内存区域有助于JVM更精确地控制内存的分配、回收、整理等操作从而提高程序的运行效率和稳定性。比如说Java程序中会有大量的对象创建和销毁为了避免频繁地进行垃圾回收JVM 使用了新生代和老年代两个区域其中新生代用于存储新创建的对象老年代用于存储生命周期较长的对象。此外JVM还提供了方法区和虚拟机栈等区域用于存储类信息和线程执行时的栈帧信息。通过划分内存区域JVM 可以更精确地控制内存的分配和释放避免了内存泄漏和内存碎片等问题。同时这种设计还可以使得GC算法更高效因为不同的区域使用不同的 GC 策略可以根据各自的特点进行优化提高GC 的效率和响应速度。 灵活管理内存在不同的应用场景下JVM需要为Java程序分配不同大小、不同生命周期的内存空间。划分不同的内存区域可以让JVM更灵活地进行内存管理满足不同的需求。 内存安全JVM的内存模型具有强大的安全机制可以保护Java程序的数据和代码不被恶意程序所破坏。通过划分不同的内存区域JVM可以更好地实现内存安全。 综上所述JVM 划分不同的内存区域主要是为了让 Java 程序的数据和代码存储在不同的内存区域中更好地管理内存提高程序的运行效率和稳定性满足不同的内存需求以及保障程序的内存安全。 (3) GC 垃圾回收器对应回收算法如下列表 Serial 收集器采用标记-清除算法。 Parallel 收集器采用标记-清除算法或标记-整理算法。 CMS 收集器采用标记-清除算法和标记-清除-整理算法在 CMS 的 initial mark 和 concurrent sweep 阶段使用标记-清除而在 concurrent mark 和 concurrent sweep 阶段使用标记-清除-整理。 G1 收集器采用标记-整理算法和复制算法通过 region 之间的内存拷贝来实现对象的移动和清理。 需要注意的是这只是一些常见的垃圾回收器和对应的回收算法实际上还有很多其他的垃圾回收器和回收算法而且不同的垃圾回收器也可能采用不同的组合方式来进行回收。 二、JMM 内存模型概述 JMMJava 内存模型 Java Memory Model简称 JMM是一种抽象的概念并不是真实存在它是描述的一组约定或者说是规范通过这组规范定义程序中各个变量读写访问方式并且决定一个线程对共享变量的读写何时让另一个线程可见关键技术点就是围绕多线程的原子性、可见性和有序性三个特性展开下面会说到什么是原子性、可见性和有序性。 由于 JVM 运行程序的实体是线程而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间)用于存储线程私有的数据而 Java 内存模型中规定所有变量都存储在主内存主内存是共享内存区域所有线程都可以访问但线程对变量的操作(读取赋值等)必须在工作内存中进行首先要将变量从主内存拷贝的自己的工作内存空间然后对变量进行操作操作完成后再将变量写回主内存不能直接操作主内存中的变量工作内存中存储着主内存中的变量副本拷贝不同的线程间无法访问对方的工作内存线程间的通信(传值)必须通过主内存来完成其简要访问过程如下图 程序运行都是靠线程驱动可以说程序运行载体就是线程JMM 规范能用来干啥 通过 JMM 来实现线程和主内存之间的抽象关系屏蔽各个硬件平台和操作系统内存访问差异以实现让 Java 程序在各个平台下都能够达到一致访问内存的效果。 假设没有 JMM 控制存在因为线程之间的副本是不能访问的并且共享数据实在主内存中的用以上方式操作变量必然存在一种数据一致性问题举个例子如下 主内存中有个变量 count初始值为 0现在线程 A 要将其加 1 操作那么肯定先要从主内存中先读取到 count 变量拷贝到私有内存中然后将 count 加 1 操作。线程 A 更新私有内存中的 count 变量值时准备将其同步会主内存但是时间是不固定的。假设线程 A 还没有来得及将 count 刷会主内存中线程 B 也将之前的 count 拷贝到自己私有内存中此时 count 还是初始值 0也将其加 1 操作最终都刷回主内存中期望值是 count 为 2但是由于没有可见性保证导致最终 count 值为 1。 这个就是线程数据脏读。那么解决这个问题就需要 JMM 规范(可见性约束)。它的关键技术点就是围绕多线程的原子性、可见性和有序性三个特性展开下面会说到什么是原子性、可见性和有序性。 三、JMM 三大特性 1、可见性 什么是可见性是指当一个线程修改了某个共享变量的值其他线程是否能够立马知道变更JMM 规定了所有的变量都是存储在主内存中。 一般一个线程想直接去修改主内存中的值是不允许的需将主内存中值拷贝到当前线程本地内存中线程对这个共享内存副本做修改修改完通过 JMM 控制写回主内存中然后通知其他线程该变量的变更操作。线程之间是无法直接访问对方工作内存中的变量线程间变量值的传递均通过主内存来完成。 比如下面这个例子 public class VisibilityDemo {private static boolean flag true;public static void main(String[] args) throws InterruptedException {Thread thread1 new Thread(() - {try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}flag false;System.out.println(Thread1 set flag to true);});Thread thread2 new Thread(() - {while (flag) {// 此处不断循环等待直到flag变量被修改}System.out.println(Thread2 detected flag change);});thread1.start();thread2.start();} }上面的代码中有两个线程分别对共享变量 flag 进行读写。在主线程中启动了这两个线程其中 thread1 线程会在休眠500ms后将 flag 设置为true而 thread2 线程会在不断循环中等待 flag 变量被修改一旦检测到 flag 变为true则会输出 Thread2 detected flag change。 但是由于没有同步机制第二个线程可能永远无法检测到第一个线程对共享变量的修改因为第一个线程对共享变量的修改可能一直存在于该线程的本地缓存中而没有及时刷新到主内存中导致第二个线程看不到这个修改。因此程序的输出结果可能是以下两种情况之一 程序无法正常退出一直处于等待状态因为第二个线程无法检测到第一个线程的修改。 输出 Thread2 detected flag change但这并不是每次运行都能出现的结果因为 flag 变量的修改可能一直存在于第一个线程的本地缓存中没有及时刷新到主内存中。 为了解决这个问题我们需要使用一些同步机制来保证可见性例如使用 synchronized 关键字或 volatile 关键字。 2、有序性 对于一个线程代码而言我们可能习惯性认为是从上往下执行有序执行但为了提高性能编译器和处理器会对指令集进行重排序。指令重排可以保证串行语义一致但是没有义务保证多线程间的语义也一致极可能产生数据脏读换句话说两行上下不相干的代码在执行有可能先执行的不是第一条不见得是从上往下执行执行顺序有可能会被优化。指令集优化可能会发生在以下阶段如下图示 1、编译器优化重排序编译器在不改变程序在单线程环境下运行的语义前提下可以重新安排语句执行顺序。目的是能够减少寄存器的读取、存储次数复用寄存器的数据。 比如下面有三条代码假设此时 A、B 在栈内刚好就是同一个地址空间那么 B 会覆盖原来 A 的值在 C 使用 A 时需要重新去读取一遍 A 的值造成性能下降 第一步A 某个计算的结果值 第二步B 某个计算的结果值 第三步C 需要使用 A 结果值来进行计算 优化重排之后如下 第一步A 某个计算的结果值 第二步C 需要使用 A 结果值来进行计算 第三步B 某个计算的结果值 这样就可以 C 就可以复用寄存器中存放的 A 值。 2、指令级并行重排序处理器多条指令并行执行如果不存在数据依赖性处理器可以改变语句对应指令执行顺序。 比如下面这两条指令完全没有数据依赖就可以并行执行提高执行效率 int a 5 int b 6 3、内存系统重排序处理器使用缓存和读写缓冲区使得数据的加载、存储操作看上去是乱序的。 单线程没啥说的最终执行结果和代码顺序执行的结果肯定是一致的。 处理器进行重排序时必须要考虑指令之间的数据依赖性那么什么是数据依赖性 比如下面这段代码 public static synchronized void sop() {int x 15; // 语句1int y 20; // 语句2x x 100;// 语句3y x * x; // 语句4}执行语句可以按照 1234、2134、1324 执行但是你不能把语句4放到语句3之前执行因为语句4需要依赖语句3先执行不能够本末倒置否则结果出错。像语句3、4之间就存在数据依赖性如果违背数据依赖性必须禁止指令重排。 数据依赖定义如果两个操作访问同一个共享变量而且这两个操作里面有一个为写操作那么这个两个操作之间就存在数据依赖是不允许重排序的。 数据依赖分成三类先假设有两个 int 变量 a、b然后看一下分类 读后写读一个变量然后再写这个变量 a b; b 1;写后写写一个变量然后继续写这个变量 a 10; a 100;写后读写一个变量后在读这个变量 a 10; b a;注意没有读后读因为数据依赖必须有一个写操作这些有数据依赖的指令是不会重排序。 2.1、什么是 as-if-serial 语义 不管有没有重排序也不关心如何进行重排序单线程环境下程序的执行结果不会改变。编译器、JVM、处理器必须最终这个语义。as-if-serial 语义保证单线程环境下执行结果不被改变happens-before 更多是保证多线程环境下执行结果不被改变。 多线程环境下存在上下交替执行由于编译器优化重排的存在两个线程中使用的变量能否保证一致性就无法确定结果无法预测。那么要怎么解决这个问题呢可以遵守 happens-before 规则。 2.2、happens-before 规则 在 JMM 内存模型中happens-before 是一种规则用于确定两个操作之间的执行顺序。如果一个操作的执行结果需要影响另一个操作的执行那么这两个操作之间必须满足 happens-before关系以确保它们之间的顺序。 具体来说如果操作A happens-before 操作B那么操作A的执行结果对操作B可见而且操作A的执行顺序早于操作B的执行顺序。happens-before 关系可以通过多种方式建立例如同步、volatile变量、线程启动和终止、线程join等。 Java 中happens-before 规则包括以下几个方面 程序次序规则在一个线程中按照程序代码的顺序前面的操作 happens-before 于后续的任何操作。 锁定规则在一个线程中释放锁之前的所有操作都 happens-before 于后续线程获取同一个锁时的所有操作。 volatile 变量规则对一个volatile变量的写操作 happens-before 于后续对该变量的读操作。 传递性规则如果A happens-before BB happens-before C则A happens-before C。 线程启动规则在一个线程内Thread对象的start()方法 happens-before 于此线程的任何操作。 线程中断规则对一个线程 interrupt() 方法的调用 happens-before 于在该线程中被中断线程的任何操作。 线程终止规则在一个线程内线程中的任何操作 happens-before 于其他线程检查到该线程已经终止的操作。 对象终结规则一个对象的初始化完成构造方法执行结束happens-before 于它的finalize()方法的开始。 这些规则描述了在 JMM 内存模型中的操作之间的 happens-before 关系保证了Java 并发程序中的正确性。 举个例子说明八个规则使用 private int value 0;public void setValue(int value){this.value value; } public void getValue(){return this.value; }假设有两个线程A、B线程A 去掉用 setValue() 方法线程B 调用 getValue() 方法那么线程B 收到的返回值是什么答案不确定 简单按以上8个 happens-before 规则分析下(规则 5,6,7,8 可以忽略没有涉及到他们的操作) 由于两个方法时不同的线程调用不在同一个线程中不满足程序次序规则两个方法都没有使用锁不满足锁定规则变量也不是使用 volatile 关键字修饰不满足 volatile 变量规则传递规则更是不满足 所以无法通过 happens-before 原则推导出线程A happens-before 线程B所以这段代码时不安全的该怎么修复这段代码呢 可以给 getValue()、setValue() 两个方法都加上 synchornized 锁把 value 变量定义成 volatile 关键字 问题如果 JMM 内存模型中的有序性都要靠 volatile、synchornized 关键字来完成那么很多程序都会变得非常啰嗦。但是一般情况下我们在编写代码时并没有时时刻刻添加这两个关键字最多在并发编程时下使用这是因为在 Java 语言中 JMM 原则下的 happens-before原则限制和规定。 总结 happens-before 就两个总原则 1. 如果一个操作 happens-before 另一个操作那么第一个操作的执行结果将会对第二个操作可见而且第一个操作的执行顺序顺序排在第二个执行之前 (对程序员的一种约束)。 2. 两个操作之间存在 happens-before 关系并不意味着一定要按照 happens-before 原子执行的顺序来执行如果重排序可以让性能更好的执行结果与它按照 happens-before 关系执行的结果一致。那么这种重排序并不非法。比如 12 21、或者轮班值日等最终结果是一致的 (对编译器、处理器重排序的一种约束)。 2.3、理解 volatile 语义 对于指令重排导致的可见性问题和有序性问题JMM 内存模型定义一套上述8个 happens-before 原则来保证多线程环境下两个操作间的原子性、可见性以及有序性。除了上述这8个原则还给开发人员开发提供 volatile、synchornized 关键字解决原子性、可见性以及有序性。volatile 另一个非常重要的作用就是禁止指令重排序。 总结成两句话如下 1. 当写一个 volatile 变量时JMM 会把线程对应的本地变量立即刷新回主内存中 2. 当读一个 volatile 变量时JMM 会把线程对应的本地变量设置无效直接从主内存中读取共享变量 2.3.1、volatile 内存语义的实现 1.字节码层面 volatile 是修饰变量的在字节码层面会添加一个标识 ACC_VOLATILE 2.JMM 层面 在哪些位置上插入内存屏障指令以及插入什么内存屏障指令。看到 volatile 变量规则表如下 2.1.volatile 变量规则 第一个操作第二个操作普通读写第二个操作volatile 读第二个操作volatile 写普通读写可以重排可以重排不可以重排volatile 读不可以重排不可以重排不可以重排volatile 写可以重排不可以重排不可以重排当第一个操作为 volatile 读时第二个操作无论什么都不允许重排序这保证 volatile 读之后的操作不会被排到 volatile 读之前。 当第二个操作为 volatile 写时第一个操作不论是什么都不允许重排序这保证 volatile 写之前的操作不会被重排序到 volatile 写之后。 当第一个操作为 volatile 写时第二个操作为 volatile 读时不能重排。 2.2.屏障指令插入策略 为什么会出现以上这种效果是因为 volatile 关键字底层自动帮你在读写操作 volatile 变量处插入了上面的四大内存屏障指令loadload、storestore、loadstore、storeload具体插入策略如下所示 volatile 写操作在每个volatile写操作之前插入storestore屏障在每个volatile写操作之后插入storeload屏障。 volatile 读操作在每个 volatile 读操作之后插入loadload屏障在每个volatile读操作之后插入loadstore屏障。 为什么要这样插入呢下面来解释下 先来看到为什么在每个volatile写操作之前插入storestore屏障在每个volatile写操作之后插入storeload屏障。如下图示 在看到为什么在每个 volatile 读操作之后插入loadload屏障在每个volatile读操作之后插入loadstore屏障。如下图示 但是为什么没有禁止普通读和 volatile 写之间的重排序 (1) volatile 写针对的是被 volatile 修饰的变量在写的时候是把 volatile 变量的值从工作内存中刷新回到主内存中。 (2) 普通读 读的一定不是被 volatile 修饰的变量。 所以普通读不会对 volatile 变量的值造成影响。也不影响相应的内存。不会去破坏 volatile 的内存语义。 简单一句话volatile 写时直接刷回主内存中读时直接从主内存中去取。那么 volatile 关键字是怎么保证程序可见性和有序性呢底层是通过内存屏障指令实现的具体什么是内存屏障指令呢 3.处理器层面 cpu 执行机器指令的时候是使用 lock 前缀执行来实现 volatile 功能的。 (1) 首先对总线/缓存加锁然后去执行后面的指令最后释放锁同时把高速缓存的数据刷新回到主内存中。 (2) 在 lock 锁住总线/缓存的时候其他 cpu 的读写请求就会被阻塞直到锁释放。lock 过后的写操作会让其他 cpu 的高速缓存中相应的数据失效这样后续这些 cpu 在读取数据时候就需要从主内存中加载最新的数据。 补充Cache line 是指缓存中的最小存储单元一般是64个字节或更多的数据块也称为cache block。当处理器需要访问某个内存地址的时候会首先检查对应的cache line是否存在如果存在处理器会直接从cache line中读取数据这样的情况被称为cache hit如果不存在处理器需要先从内存中读取相应的数据块并将其放入缓存中这个过程被称为cache miss。由于cache line的大小一般比较大一次读取可以获取到多个数据从而提高了读取效率。同时缓存的访问速度比内存快很多因此使用缓存可以有效地减少处理器对内存的访问次数提高程序的运行效率。 2.4、内存屏障指令 先看一个例子比如在节假日西湖没有人管控显得特别乱还有的人可能掉进河里如下图示 就是因为没有管控顺序难保所以需要设定规则禁止乱序——上海南京人墙隔离如下图示 这样就可以防止上面的人和下边的人乱窜在一起这个人墙就相当于是现在要说的屏障只不过现在是在内存中所以称之为内存屏障。所以 volatile 关键字是靠内存屏障保证可见性和有序性。 内存屏障Memory Barrier是指一组处理器指令用于实现对内存操作的顺序限制。内存屏障可以分为多种类型包括读屏障、写屏障、全屏障等。 在 Java 中内存屏障被广泛应用于实现内存可见性和 happens-before 规则。Java 内存模型规定在执行某个操作之前或之后插入适当的内存屏障指令可以确保程序正确性保证各个线程对共享变量的操作能够按照一定的顺序进行。 例如对于 volatile 变量的写入操作Java 内存模型要求在写入操作之后插入一个写屏障store barrier以确保该操作的结果对其他线程可见。而对于 volatile 变量的读取操作需要在读取操作之前插入一个读屏障load barrier以确保读取操作看到的是最新值。 总之内存屏障是一种非常重要的硬件和编译器技术在实现多线程编程和共享内存时扮演着关键的角色。 2.5、内存屏障分类 内存屏障可以分为多种类型包括读屏障、写屏障、全屏障。 读屏障(Load Barrier)在读操作之前插入读屏障可以保证读操作之前的所有写操作对该读操作可见即读操作看到的是最新的写操作结果。换句话说该读指令会让其工作内存/CPU高速缓存中的数据直接失效重新从主内存中读取最新数据。 写屏障(Store Barrier)在写操作之后插入写屏障可以保证该写操作对于其他线程的读操作是可见的即其他线程看到的是最新的写操作结果。换句话说该写指令会让强制把缓存区的数据刷回到主内存中。 全屏障(fullFence)全屏障是读屏障和写屏障的结合它不仅保证了写操作对于其他线程的读操作是可见的也保证了读操作看到的是最新的写操作结果。 需要注意的是内存屏障的使用需要慎重过多的使用会影响程序的性能。 再来看看内存屏障长啥样子 Unsafe.java 源码如下Java 中不安全类中有三个 native 修饰的方法都是去调用 JVM 的代码执行 public final class Unsafe {HotSpotIntrinsicCandidatepublic native void loadFence();HotSpotIntrinsicCandidatepublic native void storeFence();HotSpotIntrinsicCandidatepublic native void fullFence(); }Unsafe.cpp 源码如下图示在 HotSport 源码中底层分别调用 OrderAccess::acquire()、OrderAccess::release()、OrderAccess::fence() 方法 在继续进入到 OrderAccess 类方法内如下图示 上面这四个就是我们经常说的四大内存屏障指令: loadload、storestore、loadstore、storeload 继续往下追到 orderAccess_linux_x64.inline.hpp Linux 操作系统文件中如下图示 2.6、内存屏障指令 对于上述中看到的四大屏障指令 loadload、storestore、loadstore、storeload具体表示什么意思呢其实就是写屏障前后顺序、读屏障前后顺序进行排列组合就组成四组屏障指令。 屏障类型指令实例说明loadloadload1;loadload;load21、禁止重排序: 保证 load1 读取操作在 load2 以及后续读取之前。 2、在这里 loadload 就相当于是一个读屏障要知道在一个读指令(load2)之前插入读屏障会发生什么效果会具备读屏障功能保证让 Load2 工作内存缓存中的数据直接失效重新从主内存中读取最新数据 这里有疑问为什么只保证后面这个 load2 而不保证前面这个 load1? 如果按照程序顺序执行总有一个开头去执行获取数据的一开始 load1 缓存中根本没有数据只能去主内存中获取所以只有第二次再来读取时才需要让自己的缓存失效从主内存中重新读取数据storestorestore1;storestore;store21、禁止重排序: 在 store2 写操作以及后续操作之前必须保证 store1 已经刷回主内存中 2、在这里 storestore 就相当于是一个写屏障要知道在一个写指令(store1)之后插入写屏障会发生啥效果那就会具备写屏障的功能保证 store1 指令写出的数据强制新回主内存中loadstoreload1;loadstore;store21、禁止重排序: 在 store2 写操作以及后续操作之前必须保证 load1 已经读取完成 2、这个指令刚好和读写屏障顺序相反所以不具备读写屏障功能就只有禁止重排序功能storeloadstore1;storeload;load21、禁止重排序保证 store1 写操作已经刷回主内存中load2 读操作以及后续操作才能执行2、storeload 指令非常强大细品会发现它有这样的特点在写操作(store1)之后插入写屏障在读操作(load2)之前插入读屏障那么必然同时具备上面读屏障和写屏障的功能 上述表格中可以发现 storeload 屏障指令比较重loadload 就相当于一个读屏障storestore 相当于写屏障loadstore 只有禁止重排序功能因为和读写屏障顺序刚好相反只有 storeload 具备读屏障、写屏障、禁止重排序功能它和内存交互次数多、交互延迟大、消耗资源多。 扩展这些屏障指令并不是处理器真实的执行指令是 JMM 定义出来跨平台的指令。因为不同的硬件实现内存屏障方式并不相同JMM 为了屏蔽这种底层硬件平台的不同抽象出了这些内存屏障指令在运行的时候有 JVM 来为不同的平台生成相对应的机器码。这些内存屏障指令在不同的硬件平台上可能会做一些优化从而只支持部分 JMM 内存屏障指令比如在x86机器上就只有 storeload 是有效的其他都不支持被替换成 nop也就是空操作。 2.7、volatile 读写过程—8种原子方法 JMM定义8种原子操作是为了确保在多线程环境下对共享变量的操作是原子的即一个线程正在进行这个操作的时候其他线程不能同时进行这个操作从而保证了数据的一致性。 这8种原子操作包括 lock锁定作用于主内存的变量把一个变量标识为一条线程独占状态。 unlock解锁作用于主内存的变量把一个处于锁定状态的变量释放出来释放后可以被其他线程锁定。 read读取作用于主内存的变量把一个变量的值从主内存传输到线程的工作内存中以便随后的load操作使用。 load载入作用于线程的工作内存的变量把read操作从主内存中得到的变量值放入工作内存中的变量副本。 use使用作用于线程的工作内存的变量把工作内存中的一个变量的值传递给执行引擎。 assign赋值作用于线程的工作内存的变量把一个从执行引擎接收到的值赋值给工作内存中的变量。 store存储作用于线程的工作内存的变量把工作内存中的一个变量的值传递给主内存以便随后的write操作使用。 write写入作用于主内存的变量把store操作从工作内存中得到的变量的值写入到主内存的变量中。 通过这些原子操作的组合可以保证共享变量的访问和修改都是原子的从而避免了数据不一致的问题。 注意 但是在 use 和 assign 中间这个缝隙(CPU执行引擎处) 会出现线程竞争比如 i 这个操作底层其实是由三条指令集组成在 use 和 assign 中间执行这三条语句不能保证线程执行原子性所以还需要加锁才能够让线程保证原子性数据不会出错。 JMM定义的8种原子操作包括了对一个变量的读、写和读-改-写三种操作以及对数组的读、写和读-改-写三种操作。这些操作确保了每个操作在执行时都具有原子性但是只针对单个变量(boolean 这种)或者单个元素。 在实际开发中复合操作i这种通常涉及多个变量或者多个元素的修改这种情况下使用 JMM 提供的原子操作无法保证原子性需要使用 synchronized 或者 Lock 等同步手段来保证线程安全。因此JMM 定义的8种原子操作并不能完全保证原子性。 比如下面这个参考例子 public class VolatileNotAtomicExample {private volatile int count 0;public void increment() {count;}public int getCount() {return count;}public static void main(String[] args) throws InterruptedException {final int THREADS_COUNT 1000;final int INCREMENT_COUNT 1000;VolatileNotAtomicExample example new VolatileNotAtomicExample();Thread[] threads new Thread[THREADS_COUNT];for (int i 0; i THREADS_COUNT; i) {threads[i] new Thread(() - {for (int j 0; j INCREMENT_COUNT; j) {example.increment();}});threads[i].start();}for (int i 0; i THREADS_COUNT; i) {threads[i].join();}System.out.println(Count: example.getCount());} } 上述代码中有 1000 个线程同时对同一个 volatile 变量进行了 1000 次累加操作理论上 count 的值应该为 1000 * 1000 1000000但是实际运行结果可能会小于这个值因为 volatile 只能保证可见性不能保证原子性。 count 底层指令如下 0: iconst_0 1: istore_1 2: iinc 1, 1 5: return在 use 和 assign 中间执行这三条语句不能保证线程执行原子性。 2.8、volatile 用于 DCL DCLDouble-checked locking是一种单例模式的实现方式通过对锁进行双重检查来实现线程安全和性能优化。使用 volatile 可以保证 DCL 实现的线程安全性。 以下是一个使用 volatile 实现 DCL 的示例代码 public class Singleton {private static volatile Singleton instance;private Singleton() {}public static Singleton getInstance() {if (instance null) {synchronized (Singleton.class) {if (instance null) {instance new Singleton();}}}return instance;} }在上面的示例中instance 变量被声明为 volatile这样可以保证 instance 对于所有线程的可见性。如果没有 volatile 修饰一个线程可能会将 instance 缓存在本地内存中而不是从主内存中读取导致另一个线程无法看到该变量的最新值。同时使用双重检查加锁的方式可以避免对整个 getInstance() 方法加锁从而提高了性能。 如果不加上 volatile 修饰符那么当一个线程首次访问 getInstance() 方法时如果在 instance null 的判断中出现指令重排将会出现如下情况 线程A创建了一个 Singleton 对象并将该对象赋值给了 instance。 线程B调用 getInstance() 方法发现 instance ! null于是直接返回 instance但是这个 instance 实际上还没有完成初始化。 加上 volatile 修饰符可以禁止指令重排保证 instance 对于所有线程的可见性并且在 instance 实例化完成之前其它线程不能访问 instance 对象。这样就能够保证 DCL 实现的线程安全性。 3、原子性 在 Java 中原子性是指一个操作是不可被中断的、不可被分割的即要么这个操作已经全部执行完毕要么就还没有执行。简单来说原子性就是指一个操作要么全部执行成功要么全部执行失败不存在执行一半的情况。 在多线程编程中原子性是非常重要的概念因为多个线程可能同时访问和修改同一份数据如果没有保证原子性那么就可能会导致数据的不一致性或者出现其他异常情况。 在 Java 中对于一些基本数据类型的操作比如赋值、加减乘除等简单的操作都是原子性的也就是说这些操作不会被其他线程打断。而对于一些复杂的操作比如多个操作的组合需要使用 synchronized、Lock、Atomic 等同步机制来保证原子性以避免出现线程安全问题。 假设有两个线程同时对一个共享的计数器进行加1操作代码如下 public class Counter {private int count 0;public void increment() {count;}public int getCount() {return count;} }public class MyThread implements Runnable {private Counter counter;public MyThread(Counter counter) {this.counter counter;}Overridepublic void run() {for (int i 0; i 10000; i) {counter.increment();}} }public class Main {public static void main(String[] args) throws InterruptedException {Counter counter new Counter();Thread thread1 new Thread(new MyThread(counter));Thread thread2 new Thread(new MyThread(counter));thread1.start();thread2.start();thread1.join();thread2.join();System.out.println(Count: counter.getCount());} } 上面的代码中有一个 Counter 类表示一个计数器它有一个 increment() 方法用于对计数器进行加1操作另外还有一个 getCount() 方法用于获取计数器的当前值。 接下来定义一个 MyThread 类表示一个线程它持有一个 Counter 对象重复执行 10000 次对计数器进行加1操作。 最后在 Main 类中启动两个线程并等待它们执行完毕最终输出计数器的值。 但是由于两个线程同时对计数器进行修改所以在不加任何同步机制的情况下就会出现线程安全问题导致计数器的值不一定是正确的。 因此我们可以使用 Java 提供的原子类 AtomicInteger 来保证计数器的原子性 public class Counter {private AtomicInteger count new AtomicInteger(0);public void increment() {count.getAndIncrement();}public int getCount() {return count.get();} }这样就可以保证计数器的加1操作是原子性的从而避免出现线程安全问题。