自己做的电影网站打开很慢,建设音乐网站,网站正在建设中单页,wordpress内网和外网在JAVA中#xff0c;线程有原子性、可见性和有序性三大特性。 1.原子性 1.1 定义 对于涉及共享变量的操作#xff0c;若该操作从其执行线程以外的任意线程来看都是不可分割的#xff0c;那么我们就说该操作具有原子性。它包含以下两层含义#xff1a; 访问#xff08;读、… 在JAVA中线程有原子性、可见性和有序性三大特性。 1.原子性 1.1 定义 对于涉及共享变量的操作若该操作从其执行线程以外的任意线程来看都是不可分割的那么我们就说该操作具有原子性。它包含以下两层含义 访问读、写某个共享变量的操作从其执行线程以外的其他任何线程来看该操作要么已经执行结束要么尚未发生即其他线程不会看到该操作的中间部分的结果。 访问同一组共享变量的原子操作不能被交错执行。 1.2 非原子性协定 在 Java 语言中除了 long 类型 和 double 类型以外的任何类型的变量的写操作都是具有原子性的但对于没有使用 volatile 关键字修饰的 64 位的 long 类型和 double 类型允许将其的读写操作划分为两次 32 位的操作来进行这就是 long 和 double 类型的非原子性协定 ( Nonatomic Treatment of double and long Variables ) 。 1.3 保证原子性 通过 Java 虚拟机规范和非原子性协定 Java 语言可以保证对基本数据类型的访问具有原子性如果想要保证更大范围内的原子性如多行操作的原子性此时可以使用字节码指令 monitorenter 和 monitorexit 来隐式执行 lock 和 unlock 操作从而将串行变成并行来保证原子性monitorenter 和 monitorexit 这两个字节码指令反映到 Java 代码中就是 synchronized 关键字。 2.可见性 2.1 定义 如果一个线程对某个共享变量进行更新之后后续访问该变量的其他线程可以读取到这个更新结果那么我们就称该更新对其他线程可见反之则是不可见这种特性就是可见性。出现可见性问题往往意味着线程读取到了旧数据这会导致更新丢失从而导致运行结果与预期结果存在差异。可见性问题与计算机的存储结构和 Java 的内存模型都有着密切的关系。 2.2 高速缓存 由于现代处理器对数据的处理能力远高于主内存DRAM的访问速率为了弥补它们之间在处理能力上的鸿沟通常在处理器和主内存之间都会存在高速缓存Cache。高速缓存相当于一个由硬件实现的容量极小的散列表Hash Table其键是一个内存地址其值是内存数据的副本或者准备写入内存的数据。 现代处理器一般具有多个层次的高速缓存如一级缓存L1 Cache、二级缓存L2 Cache、三级缓存L3 Cache等。其中一级缓存通常包含两部分其中一部分用于存储指令L1i另外一部分用于存储数据L1d。距离处理器越近的高速缓存其存储速率越快制造成本越高因此其容量也越小。在 Linux 系统中可以使用 lscpu 命令查看其高速缓存的情况。 2.3 缓存一致性协议 在多线程环境下每个线程运行在不同的处理器上当多个线程并发访问同一个共享变量时这些线程的执行处理器都会在自己的高速缓存中保留一个该共享变量的副本这种情况下如何让一个处理器对数据的更改能被其他处理器感知到为了解决这个问题需要引入一种新的通讯机制这就是缓存一致性协议。 缓存一致性协议有着多种不同的实现这里以广泛使用的 MESI (Modified-Exclusive-Shared-Invalid) 协议为例和其名字一样它将高速缓存中的缓存条目分为以下四种状态 Invalid该状态表示相应的缓存行中不包含任何内存地址对应的有效副本数据。 Shared该状态表示相应的缓存行中包含相应内存地址所对应的副本数据并且其他处理器的高速缓存中也可能存在该相同内存地址所对应的副本数据。 Exclusive该状态表示相应的缓存行中包含相应内存地址所对应的副本数据但其他处理器的高速缓存中不应存在该相同内存地址所对应的副本数据即独占的。 Modified该状态表示相应的缓存行中包含对相应内存地址所做的更新的结果。MESI 协议限制任意一个时刻只能有一个处理器能对同一内存地址上的数据进行更新因此同一内存地址在任意一个时刻只能有一个缓存条目处于该状态。 根据以上状态当某个处理器对共享变量进行读写操作时其具体的行为如下 读取共享变量处理器首先在高速缓存上进行查找如果对应缓存条目的状态为 ME 或者 S此时则直接读取如果缓存条目为无效状态 I此时需要向总线发送 Read 消息其他处理器或主内存则需要回复 Read Response 来提供相应的数据处理器在获取到数据后将其存储到相应的缓存条目并将状态更新为 S 。 写入共享变量此时处理器首先需要判断是否拥有对该数据的所有权如果对应缓存条目的状态为 E 或者 M代表此时均处于独占状态此时可以直接写入并将其状态变更为 M 。如果不为 E 或 M此时处理器需要往总线上发送 Invalidate 消息来通知其他处理器将对应的缓存条目失效之后在收到其他处理器的 Invalidate Acknowledge 响应后再进行更改并将其状态变更为 M。 在只有高速缓存的情况下通过缓存一致性协议能够保证一个线程对共享变量的更新对于其他线程是可见的。如果只是这样多线程编程就不会存在可见性问题了但实际上缓存一致性协议并不能保证最终的可见性这是由于写缓冲器和无效化队列导致的。 2.4 写缓冲器与无效化队列 在上面的缓存一致性协议中处理器必须等待其他处理器的应答Read Response \ Invalidate Acknowledge后才去执行后续的操作这会带来一定的时间开销为了解决这个问题现代计算机架构又引入了写缓冲器和无效化队列 写缓冲器当处理器发现缓存条目的状态不为 E 或 M 时此时不再等待其他处理器返回 Invalidate Acknowledge 消息而是直接将变更写入到写缓冲器就认为操作完成。当收到对应的 Invalidate Acknowledge 消息再将变更写入到对应的缓存条目中此时写操作对于其他处理器而言才算完成。 无效化队列当其他处理器接收到 Invalidate 消息后不再等待删除指定缓存条目中的副本数据后再回复 Invalidate Acknowledge 而是将消息存入到无效化队列中后就直接回复之后处理器再根据无效化队列中的消息来重置缓存行的状态到 Invalid 。 写缓冲器是处理器的私有部件一个处理器的写缓冲器所存储的内容是不能被其他处理器所读取的这就会导致一个更新即便已经发生并写入到写缓冲器但是其他处理器上的线程读取到的还是旧值从而导致可见性问题。除了写缓冲器外无效化队列也会导致可见性问题当某个写入发生后其他处理器上的对应缓存条目应该都立即失效但是由于无效化队列的存在Invalidate 操作不会立即执行导致其他处理器仍然读取到的仍然是未失效的旧值。 2.5 内存屏障 想要解决写缓存器和无效化队列带来的问题需要引入一个新的机制 —— 内存屏障 Store Barrier存储屏障可以使执行该指令的处理器冲刷其写缓冲器。 Load Barrier加载屏障将无效化队列中所指定的缓存条目的状态都标志位 I 并清空无效化队列从而保证处理器在读取共享变量时必须发送 Read 消息去获取更新后的值。 冲刷写缓冲器和清空无效化队列都是存在时间消耗的所以只有在必须要保证可见性的场景下才应该去使用内存屏障。何种场景下必须要保证可见性这是由用户来决定的这也是多线程编程所需要考虑的问题。 2.6 保证可见性 在 Java 语言中保证可见性的典型实现是 volatile 关键字它在 Java 语言中一共有三种作用 保证可见性Java 虚拟机JIT 编译器会在 volatile 变量写操作之后插入一个通用的 StoreLoad 屏障它可以充当存储屏障来清空执行处理器的写缓冲器同时 JIT 编译器还会在变量的读操作前插入一个加载屏障来清空无效化队列。 禁止指令重排序通过内存屏障 Java 虚拟机可以保证 volatile 变量之前的任何读写操作都先于这个 volatile 写操作之前被提交而 volatile 变量的读操作先于之后任何变量的读写操作被提交。 除了以上两类语义外Java 虚拟机规范还特别规定了对于使用 volatile 修饰的 64 的 long 类型和 double 类型的变量的读写操作具有原子性。 除了 volatile 外synchronized 和 final 关键字都能保证可见性 synchronized synchronized 关键字规定了对其所修饰的变量执行 unlock 操作前必须先把此变量同步回主内存中。 final 被 final 修饰的字段在构造器中一旦初始化完成并且构造器没有把 this 的引用逃逸到外部那么其他线程中就能看到 final 字段的值即可见性得到保证。 2.7 Java 内存模型 以上主要介绍计算机的内存模型对可见性的影响但是不同架构的处理器在内存模型和支持的指令集上都存在略微的差异。 Java 作为一种跨平台的语言必须尽量屏蔽这种差异而且还要尽量利用硬件的各种特性如寄存器高速缓存和指令集中的某些特有指令来获取更好的执行速度这就是 Java 的内存模型 Main Memory主内存Java 内存模型规定了所有的变量都存储在主内存中主内存可以类比为计算机的主内存但其只是虚拟机内存的一部分并不能代表整个计算机内存。 Work Memory工作内存Java 内存模型规定了每条线程都有自己的工作内存工作内存可以类比为计算机的高速缓存。工作内存中保存了被该线程使用到的变量的拷贝副本。 线程对变量的所有操作都必须在工作内存中进行而不能直接读写主内存中的变量不同的线程之间也无法直接访问对方工作内存中的变量线程间变量值的传递需要通过主内存来完成。 3.有序性 3.1 顺序语义 Java 语言中的顺序语义可以分为以下四类 源代码顺序程序员编写的代码的执行顺序 程序顺序编译后的代码的执行顺序 执行顺序给定代码在处理器上的实际执行顺 感知顺序处理器感知到的其他处理器上代码的执行顺序。 3.2 重排序类型 编译器和处理器出于性能考虑通常会改变代码的实际执行顺序这种情况就称为重排序具体分为以下两类 重排序类型 重排序表现 重排序主体(原因) 指令重排序 程序顺序和源代码顺序不一致 编译器 执行顺序和程序顺序不一致 JIT 编译器、处理器 存储子系统重排序 内存重排序 源代码顺序、程序顺序和执行顺序这三者保持一致 但是感知顺序与执行顺序不一致 高速缓存、写缓冲器 3.3 貌似串行语义 尽管编译器和处理器都能够进行指令重排序但它们都必须遵循 貌似串行语义As-if-serial Semantics即重排序不能影响程序在单线程上执行结果的正确性。按照 As-if-serial 原则只有不存在数据依赖关系的语句才会被重排序存在数据依赖关系的语句不会被重排序示例如下。此时第 12 行语句彼此之间可以进行重排序但是第 3 行语句不能被重排序到 1 和 2 行语句之前 int a 1;
int b 2;
int c a b; 同时为了保证单线下执行的正确性处理器会将重排序指令的执行结果先写入到重排序缓冲器ROBRecorder Buffer中之后再按照这些指令被处理器读取的顺序提交到寄存器或者主内存中因此虽然指令是乱序执行的但结果却是顺序提交的从而能够保证在单线程下的正确性。 3.4 内存重排序 由于写缓冲器和高速缓存的存在并且写缓冲器是不能被其他处理器所访问的因此其他处理器感知到的顺序可能仍然与执行顺序不同这种情况就叫做内存重排序。但需要说明的是指令重排序是一种实实在在的重排序它改变了指令的执行顺序但内存重排序只是一种现象只是其他处理器的错觉。 3.5 保证顺序性 在 Java 语言中volatile 和 synchronized 都能够保证有序性 volatile通过内存屏障来禁止指令重排序通过加载屏障和存储屏障来冲刷写缓冲器和清空无效化队列从而可以避免内存重排序的现象 synchronized 使用 synchronized 修饰的变量在同一时刻只允许一个线程对其进行 lock 操作这种限制决定了持有同一个锁的两个同步块只能串行执行也就避免了乱序问题。