Java并发编程第5讲——volatile关键字(万字详解)
volatile关键字大家并不陌生,尤其是在面试的时候,它被称为“轻量级的synchronized”。但是它并不容易完全被正确的理解,以至于很多程序员都不习惯去用它,处理并发问题的时候一律使用“万能”的sychronized来解决,然而如果能正确地使用volatile的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。
下面我们从volatile关键字的定义说起。
一、什么是volatile关键字
volatile关键字可以说是Java虚拟机提供的最轻量级的同步机制,和synchronized不同,volatile是一个变量修饰符,只能用来修饰变量,用法也比较简单,只需要在声明一个可能被多线程方法的变量时,使用volatile修饰即可。
但是,在并发编程的三大特性——原子性、可见性、有序性中,volatile只能保证可见性和有序性(禁止指令重排),并不能保证原子性,而synchronized这三种特性都可以保证。
那么,volatile为什么不能保证原子性,而synchronized可以?还有可见性和有序性它俩又是怎么保证的呢?别急,接着往下看。
二、Java内存模型
由于volatile关键字与Java内存模型有较多的关联,所以在详细介绍volatile关键字之前,需要先了解一下Java内存模型。
2.1 什么是Java内存模型
Java内存模型(Java Memory Model,简称JMM)是Java虚拟机(JVM)对多线程程序中的内存访问和操作进行规范的一种抽象,并不真实存在。它定义了线程如何与主存(共享内存)和工作内存(线程私有内存)进行交互,以及如何同步和互斥地访问共享数据。
简单地说就是JMM定义了程序中各种变量(共享)的访问规则,即关注在虚拟机中把变量值存储到内存和从内存中取出变量值这样的底层细节。
《Java虚拟机规范》中曾试图定义一种“Java内存模型”来屏蔽各种硬件和操作系统的内存范根差异,以实现让Java程序在各种平台下都能达到一致的访问效果,但这并非是一件易事,这个模型必须定义的足够严谨,才能让Java的并发内存访问操作不会产生歧义;但是也必须定义得足够宽松,使得虚拟机得实现能有足够得自由空间去利用硬件得各种特性(寄存器、高速缓存和指令集中某些特有的指令)来获取更好的执行速度。Java内存模型自JDK1.2建立起来,随后又经过长时间的验证和修补,直到JDK5(JSR-133)发布后,也就是目前正在使用的Java内存模型,才终于成熟、完善起来了。
ps:JSR-133对旧内存模型的修补主要有两个:
- 增强volatile的内存语义:旧内存模型允许volatile变量与普通变量重排序。JSR-133严格限制volatile变量与普通变量的重排序,使volatile的写-读和锁的释放-获取具有相同的内存语义。
- 增强final的内存语义:在旧内存模型中,多次读取同一个final变量的值可能会不相同。为此,JSR-133为final增加了写和读重排序规则。在保证final引用不会从构造函数内逃逸出的情况下,final具有了初始化安全性。
2.2 主内存与工作内存
Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存保存了被该线程使用到的变量的主内存副本,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的数据。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。线程、主内存、工作内存三者的交互关系如下图:
2.3 内存间交互操作
ps:这里做个简单了解即可,因为除了虚拟机开发团队外,大概没有其他开发人员会以这种方式来思考并发问题。下面会介绍该部分内容的等效判断原则——先行发生规则(happens-before),相较于这种方式更容易理解。
关于主内存与工作内存之间具体的交互协议,Java虚拟机定义了8种原子操作:
- lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
- unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其它线程锁定。
- read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
- load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用的变量的值的字节码指令时,执行这个操作。
- assign(赋值):作用于工作内存的变量,它把一个执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时,执行这个操作。
- stroe(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
- write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
由上可见,如果要把一个变量从主内存复制到工作内存,那就要顺序执行read和load操作,而要把变量从工作内存同步回主内存,就要顺序执行strore和write操作。注意,这里是顺序执行而不代表它们会连续执行,如对主内存中的变量a、b进行访问时,可能出现的顺序是:read a、read b、load b、load a。
除此之外,针对上述8种基本操作,Java内存模式还制定了8种规则:
- 不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况出现。
- 不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
- 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。
- 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说,就是对一个变量实施use、store操作之前,必须先执行过了assign和load操作。
- 一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
- 如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
- 如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定住的变量。
- 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。
三、原子性、可见性和有序性
读到这,相信你已经对Java内存模型有了一定了解,那么下面我们就基于Java内存模型来介绍一下Java并发的三大特性:原子性、可见性和有序性。
3.1 原子性
3.1.1 什么是原子性
定义:一个或多个操作不可拆分、不被中断。
ps:数据库中ACID的原子性指的是“要么都执行要么都回滚”,这是两个不同的概念。
首先说一句,volatile并不能保证原子性。
ps:synchronized关键字可以保证原子性,因为被synchronized修饰的方法或代码块,在进入之前都加了锁,同一时刻,有且仅有一个线程能执行被“锁”住的代码片段,这就保证了它内部的代码可以全部被执行,所以它具备原子性(基于monitorenter和monitorexit指令实现)。
3.1.2 举例说明
先举一个例子,加深一下对Java并发编程中原子性的理解。
//代码1
int a = 1;
//代码2
a++;
//代码3
int a = b;
//代码4
k =k + 1;
问:上述4个代码哪些是原子操作?你可能会说是代码1,但我告诉你,上述均不是原子操作!下面我们来分析一下(多线程情况下):
- 代码1:别看它只是简单的赋值操作,在JMM中包含了两个操作,一是从主存中读取a的值到工作内存,二是在工作内存中将a的值设置为1,这些步骤可以被其它线程中断,所以它不具备原子性(下面就简单说了)。
- 代码2:包含了三个操作。一是读取变量a的值,二是把变量a的值加一,三是将计算后的值再赋值给变量a。
- 代码3:包含了两个操作。一是读取变量b的值,二是将变量b的值赋值给a。
- 代码4:包含三个操作。一是读取变量k的值,二是将变量k的值加一,三是将计算后的值再赋值给变量k。
从Java内存模型来看,直接保证的原子性变量操作包括read、load、assign、use、store和write,我们大致可以认为基本数据类型的访问读写时具备原子性的。
从应用场景来看,JVM保证原子性操作的主要方式如下:
虽然Java内存模型还提供了lock和unlock操作来满足原子性的需求,但并未对用户开放使用,而是提了更高层次的字节码指令monitorenter和monitorexit来隐式地使用这两个操作,也就是我们熟悉的同步块——synchronized关键字。
AQS的锁机制:比如ReentrantLock、ReentrantReadWriteLock等。
CAS实现,java.util.concurrent.atomic包下的原子操作类,比如AtomicInteger、AtomicLong等。
3.1.3 为什么volatile不具备原子性
那volatile关键字为什么不能保证原子性呢?很简单的一点就是它不是锁,而且也没做任何可以保证原子性的处理,这当然不能保证原子性了。
下面看个经典的i++案例:
public class TestIncr {volatile int num = 0;public void add() {num++;}public static void main(String[] args) {TestIncr test = new TestIncr();//启动10个线程for (int i = 0; i < 10; i++) {new Thread(() -> {//每个线程执行1000次+1操作for (int j = 0; j < 1000; j++) {test.add();}}, String.valueOf(i)).start();}while (Thread.activeCount() > 2) {Thread.yield();}System.out.println("最后num的值为:" + test.num);}
}
多次执行的其中一个结果:
以上代码,我们的预期结果应该是10000才对,但执行起来发现,并不是每次都是10000,这就是因为i++这个操作没办法保证原子性。它其实包含三个指令:
执行GETFIELD拿到主内存中的原始值num。
执行IADD进行+1操作。
执行PUTFIELD把工作内存中的值写回主内存中。
当多个线程并发执行PUTFILED指令的时候,会出现写回主存覆盖的问题,所以最终结果可能会比预期的结果要小,所以volatile不能保证原子性。
3.2 可见性
volatile的两大特性之一就是可以保证可见性,下面我们就详细介绍一下。
3.2.1 定义及实现
定义:指多个线程之间共享数据的可见性。即当一个线程修改了共享变量时,其它线程能立即看到这个修改。
当对volatile变量进行写操作的时候,JVM会向处理器发送一条Lock前缀的指令,将这个缓存中的变量写回到系统主存中。
所以,如果一个变量被volatile修饰的话,在每次数据变化之后,其值都会被强制刷入主存。而其它处理器的缓存由于遵守了缓存一致性协议(比如MSI、MESI、MOSI、Synapse、Firefly及Dragon Protocaol等),也会把这个变量的值从主存加载到自己的缓存中,这就保证了一个volatile在并发编程中,其值在多个缓存中是可见的。
ps:synchronized的可见性是由上述八种规则中的——“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)”这条规则实现的。
3.2.2 举例说明
下面是一个简单示例,先看看不用volatile的效果:
public class TestVisibility {private boolean flag = false;public void start() {new Thread(() -> {System.out.println("Thread 1 start");while (!flag) {// 不断循环,等待flag变为true}System.out.println("Thread 1 complet");}).start();// 确保线程1先启动try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {System.out.println("Thread 2 start");// 修改flag的值为trueflag = true;System.out.println("Thread 2 complet");}).start();}public static void main(String[] args) {TestVisibility example = new TestVisibility();example.start();}
}
运行结果:
从结果看,程序“卡”在了Thread 1的while循环中,说明Thread 1读到的flag还是flase,但是Thread 2已经把它改为true了,这是为什么?这是因为Thread 1在执行的时候,就把flag的副本保存在这就的工作内存中,之后就会一直读取自己线程工作内存中flag变量的值,而不会去主内存中重新获取新的值。
加了volatile修饰后的运行结果:
可以看到Thread 1和2都执行完了,volatile能确保对flag的写操作立即刷新到主内存,并且对flag的读操作会从主内存中获取最新的值。
3.3 有序性
volatile除了可以保证数据的可见性之外,还有一个强大的功能,那就是它可以禁止指令重排序,所以能在一定程度上保证有序性。
3.3.1 定义和实现
定义:一个线程中的所有操作必须按照程序的顺序来执行。
volatile的有序性是它本身的特性——禁止指令重排实现的,而禁止指令重排又是由内存屏障来实现的(下面有介绍)。
ps:synchronized的有序性则是由“一个变量在同一时刻只允许一条线程对其进行Lock操作”这条规则实现的。
3.3.2 举例说明
最经典的例子当然是双重检测实现单例的例子了,如下:
public class Singleton {//私有化构造函数private Singleton(){}//单例对象(无volatile修饰)private static Singleton instance=null;public static Singleton getInstance(){//第一次检测if (instance==null){//加锁synchronized (Singleton.class){//第二次检测if (instance==null){//初始化instance=new Singleton();}}}return instance;}
}
以上代码,我们通过使用synchronized对Singleton.class加锁,可以保证同一时间只有一个线程可以执行到同步代码块中的内容,这就实现了一个单例。
但是,在极端情况下,上述的单例对象可能发生空指针异常,那么这是如何发生的呢?
我们假设线程1和线程2同时请求getSingleton()方法的时候:
- 线程1执行到
instance=new Singleton();
,开始初始化。- 线程2执行到“第一次检测”的位置,判断singleton == null。
- 线程2经过判断发现singleton !=null,于是就直接执行
return instance
。- 线程2拿到singleton对象后,开始执行后续的操作。
以上过程看似没有什么问题,但在第4步执行后续操作的时候,是有可能抛空指针异常的,这是因为在第3步的时候,线程2拿到的singleton对象并不是一个完整的对象。
很明显instance=new Singleton();
,这段代码出现了问题,那我们来分析一下,这个代码的执行过程可以简化成3步:
- JVM为对象分配一块内存M。
- 在内存上为对象进行初始化。
- 将内存M的地址赋值给singleton变量。
因为将内存的地址赋值给singleton变量是最后一步,所以线程1在这一步骤执行之前,线程2在对singleton == null判断一直都是true,那么它会一直阻塞,直到线程1执行完。
但是这个过程并不是一个原子操作,并且编译器可能会进行重排序,如果以上步骤被重排序为:
- JVM为对象分配一块内存M。
- 将内存M的地址赋值给singleton变量。
- 在内存上为对象进行初始化。
这样的话线程1会先内存分配,再执行变量赋值,最后执行初始化。也就是说在线程1执行初始化之前,线程2对singleton == null的判断会提前得到一个false,于是便返回了一个不完整的对象,所以在执行后续操作时,就发生了空指针异常。
很明显,这是指令重排造成的问题,要解决的话,直接禁止它指令重排就行了,所以volatile就派上用场了,只需要用volatile修饰一下instance即可,这里代码就不做展示了。
3.4 有了synchronized为什么还需要volatile
介绍完并发中的三大特性,我们发现在这三种特性中,synchronized总能作为其中的一种解决方案,看起来很“万能”对吧😁。不过确实是这样,绝大部分的并发控制,都能用synchronized来完成,这就是出现“遇事不决,就synchronized”的原因。
虽然synchronized很“万能“,但它毕竟是锁,那么既然是锁,它天然就具备以下缺点:
- 有性能损耗:虽然在JDK1.6种对synchronized做了很多优化,比如适应性自选、锁消除、锁粗化、轻量级锁和偏向锁等。但它毕竟是一种锁,所以,无论是同步方法还是代码块,在同步操作之前还是要进行加锁,同步操作之后解锁,这个加锁和解锁的过程都是有性能损耗的。
- 产生阻塞:无论是同步方法还是代码块,换句话说,无论是ACC_SYNCHRONIZED还是monitorenter和monitorexit都是基于Monitor实现的。基于Monitor对象,当多个线程同时访问一段同步代码时,首先会进入Entry Set,当有一个线程获取到对象的锁之后,才能进行The Owner区域,其他线程还会继续等待。并且当某个线程调用了wait方法后,会释放锁并进入Wait Set等待,所以synchronized实现的锁本质上是一种阻塞锁。
所以volatiile比synchronized的性能更好,除此之外,volatile还有一个很好的附加功能,就是可以禁止指令重排,volatile借助内存屏障来帮助其解决可见性和有序性问题,有一个典型的例子就是3.3.2小节的双重校验锁实现的单例模式,在没有volatile修饰的intance变量时,可能会发生空指针异常,有volatile修饰时就可以用volatile禁止指令重排的特性完美解决此问题。
四、指令重排序
上面在介绍有序性的时候提到了“重排序”,这里介绍一下。
4.1 什么是重排序
定义:指在保证最终结果不受影响的前提下,可以改变程序中指令的执行顺序,以达到提高代码执行效率的效果。
具体来说,指令重排可能会包括以下几种情况:
- 编译器优化:编译器在生成目标代码时可以对指令进行重排。
- 处理器优化:处理器可以根据指令之间的依赖性重排指令的执行顺序,以便更有效地使用处理器资源。
- 内存系统优化:处理器可以利用缓存和读写缓冲区等机制来重排对内存的读和写操作。
4.2 数据依赖性
如果两个操作访问同一变量,且这两个操作中有一个是写操作,此时这两个操作之间就存在数据依赖性。数据依赖分为3种类型,如下图:
上面的三种情况,在单线程的情况下,只要重排序了两个操作的执行顺序,就会改变最终结果,因此这三种情况是不会被重排序的。
int a = 1;
int b = 2;
上面这段代码的两个操作并没有数据依赖性,改变两者的执行顺序也不会影响最终结果,因此有可能被重排序。
ps:补充一个as-if-serial语义——不管怎么重排序,(单线程)程序的执行结果不能改变。编译器、runtime和处理器都必须遵守as-if-serial语义。为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖性关系的操作做重排序。
五、内存屏障(Memory barrier)
上面在介绍有序性的时候也提到了“内存屏障”,volatile禁止指令重排的特性就是基于内存屏障来实现的,下面我们来看一看。
5.1 什么是内存屏障
概念:在Java中,内存屏障是一种机制,用于控制指令重排和内存可见性,确保多线程程序的操作顺序和一致性。
5.2 volatile变量的内存屏障
volatile变量的内存屏障是通过一组指令来实现的,包括LoadLoad、LoadStore、StoreStore和StoreLoad。这些指令用于保证在volatile变量的读取和写入操作中,相邻指令之间顺序不会被改变:
- LoadLoad:确保在读取一个volatile变量前,前面的所有读操作都已经完成。
- LoadStore:确保在读取一个volatile变量后,后面的所有写操作都还没有开始。
- StoreStore:确保在写入一个volatile变量前,前面的所有写操作都已经完成。
- StoreLoad:确保在写入一个volatile变量后,后面的所有读操作都还没有开始。
当一个线程执行一个读取volatile变量的操作时,Java会插入LoadLoad和LoadStore屏障。LoadLoad屏障会防止该读操作和前面的任何读取操作被重排序,LoadStore屏障则会防止该读取操作和后续的写入操作被重排序。
当一个线程执行一个写入volatile变量的操作时,Java会插入StoreStore和StoreLoad屏障。StoreStore屏障会防止该写入操作和前面的任何写入操作被重排序,StoreLoad屏障则会防止该写入操作和后续的读取操作被重排序。
5.3 举个例子
public class VolatileTest {private volatile int value = 0;public void setValue(int newValue){//Store操作value = newValue;}public int getValue(){//Load操作return value;}
}
在上面例子中,由于value变量被volatile修饰,所以编译器和JVM会在编译和执行过程中插入内存屏障:
- 当执行setValue()方法时,编译器会插入StoreStore屏障,确保value被修改之前,所有的写操作都已经完成。然后,编译器会插入StoreLoad屏障,确保在value被修改之后,所有的读操作都还没开始。
- 当执行getValue()方法时,编译器会插入LoadLoad屏障,确保在读取value之前,前面的所有读操作都已经完成。然后,编译器会插入LoadStore屏障,确保在读取value之后,后面的所有写操作都还没有开始。
通过插入这写内存屏障,Java确保了voaltile变量的可见性和禁止重排序,从而使多线程访问volatile变量时能够正确地同步数据。
六、先行发生原则(happens-before)
在2.3节——内存间交互操作中,提到了happens-before原则。它是JMM最核心的概念,它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,我们可以通过几条规则解决并发环境下两个操作之间是否可能存在冲突的所有问题,因此,对于Java程序员来说,理解happens-before是理解JMM的关键。
6.1 什么是happens-before原则
概念:“happens-before”原则是Java内存模型中的一种规则,用于确定在多线程环境下,对于多个操作之间的执行顺序和可见性。
如果说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等。
6.2 举个例子
举个例子来加深对happens-before原则的理解:
k = 1;//线程A中执行
j = k;//线程B中执行
k = 2;//线程C中执行
假设线程A中的操作“k=1”先行发生于线程B的操作“j=k”,那么可以确定在线程B的操作执行后,变量j的值一定是1,得出这个结论的依据有两个:一是线程发生原则,“k=1”的结果可以被观察到;而是线程C还没“登场”,线程A操作结束之后没有其它线程会修改k的值。
现在来考虑线程C,我们依然保持线程A和线程B的先行发生关系,而线程C出现在线程A和线程B操作之间,但是线程C与线程B没有先行发生关系,那j的值会是多少呢?答案是不确定!1和2都有可能,因为线程C对变量k的影响可能会被B观察到,也可能不会,这时候线程B就存在读取到过期数据的风险,不具备多线程安全性。
6.3 happens-before规则
《JSR-133:Java Memory Model and Thread Specification》定义了如下happens-before规则,也是Java内存模型下“天然的”先行发生关系,这些先行发生关系无须任何同步器就已经存在,可以在编码中直接使用。如果两个操作之间的关系不在此列,并且无法从下列规则推导出来的话,它们就没有顺序性保障,虚拟机可以对它们随意地进行重排序:
- 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
- 监视器锁规则:对于一个锁的解锁,happens-before于随后对这个锁的加锁。
- volatile变量规则:对于一个volatile域的写,happens-before于任意后续这个volatile域的读。
- 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
- start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
- join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
- interrupted()规则:对线程interrupt()方法的调用happens-before于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
- 对象终结规则:一个对象初始化完成(构造函数执行结束)happens-before它的finalize()方法的开始。
Java语言无须任何同步手段保障就能成立的先行发生规则就只有上面这些了,下面演示一下如何使用这些规则去判定操作间是否具备顺序性,对于读写共享变量的操作来说,就是线程是否安全,读者还可以从下面这个例子中感受一下“时间上的先后顺序”与“先行发生”之间有什么不同。
public class VolatileTest {private int value = 0;public void setValue(int newValue){value = newValue;}public int getValue(){return value;}
}
上面的代码是一组再普通不过的getter/setter方法,假设存在线程A和B,线程A先(时间上的先后)调用了“setValue(1)”,然后线程B调用了同一个对象的“getValue()”,那么线程B收到的返回值是什么?
我们依次分析一下先行发生原则中的各项规则,由于两个方法分别由线程A和线程B调用,不在一个线程中,所以程序次序规则在这里不适用;由于没有同步块,自然就不会发生lock和unlock操作,所以管程锁定规则不适用;由于value变量没有被volatile关键字修饰,所以volatile变量规则不适用;后面的线程启动、终止、中断规则和对象终结规则也和这里完全没有关系。因为没有一个适用的先行发生规则,所以最后一条传递性也无从谈起,因此我们可以判定尽管线程A在操作时间上先于线程B,但是无法确定线程B中“getValue()”方法的返回结果,换句话说,这里面的操作不是线程安全的。
那怎么修复这个问题呢?我们至少有两种比较简单的方案可以选择:要么把getter/setter方法都定义为synchronized方法,这样就可以套用管程锁定规则;要么把value定义为volatile变量,由于setter方法对value的修改不依赖value的原值,满足volatile关键字使用场景,这样就可以套用volatile变量规则来实现先行发生关系。
通过上面的例子,我们可以得出结论:一个操作“时间上的先发生”不代表这个操作会是“先行发生”,那如果一个操作“先行发生”是否就能推导出这个操作必定是“时间上的先发生”呢?很遗憾,这个推论也是不成立的,一个典型的例子就是多次提到的“指令重排序”,演示例子如下代码所示。
//以下操作在同一个线程中执行
int i = 1;
int j = 2;
代码清单的两条赋值语句在同一个线程之中,根据程序次序规则,“int i=1”的操作先行发生于“int j=2”,但是“int j=2”的代码完全可能先被处理器执行,这并不影响先行发生原则的正确性,因为我们在这条线程之中没有办法感知到这点。
上面两个例子综合起来证明了一个结论:时间先后顺序与先行发生原则之间基本没有太大的关系,所以我们衡量并发安全问题的时候不要受到时间顺序的干扰,一切必须以先行发生原则为准。
七、总结
本篇文章从volatile的定义谈起,由于volatile与Java内存模型有较多的关联,所以接着介绍Java内存模型的相关概念、线程、主内存和工作内存之间的关系以及内存间的交互规则;随后详细介绍了volatile和synchronized在Java并发编程的三大特性——原子性、可见性和有序性中的表现(重点介绍volatile)。最后介绍了一下“重排序”、“内存屏障”和“先行发生原则(happens-before)”等重要概念。
End:希望对大家有所帮助,如果有纰漏或者更好的想法,请您一定不要吝啬你的赐教🙋。
相关文章:
Java并发编程第5讲——volatile关键字(万字详解)
volatile关键字大家并不陌生,尤其是在面试的时候,它被称为“轻量级的synchronized”。但是它并不容易完全被正确的理解,以至于很多程序员都不习惯去用它,处理并发问题的时候一律使用“万能”的sychronized来解决,然而如…...
6.小程序api分类
事件监听 以on开头,监听某个事件触发,例如:wx.WindowResize事件 同步 以Sync结尾的是同步,可以通过函数返回值直接获取,例如:wx.setStorageSync 异步 需要通过函数接收调用结果,例如&#…...
什么是PPS和TOD时序?授时防护设备是什么?
介绍 PPS和TOD PPS和TOD是两种用于精确时间同步的技术,它们在许多领域都有广泛的应用,总的来说,PPS和TOD被广泛应用于各种需要高度精确时间同步的领域,包括通信、测量、测试、系统集成和计算机网络等。 一、PPS PPS(…...
推荐一款好用的开源视频播放器(免费无广告)
mpv是一个自由开源的媒体播放器,它支持多种音频和视频格式,并且具有高度可定制性。mpv的设计理念是简洁、高效和功能强大。 软件特点: 1. 开源、跨平台。可以在Windows\Linux\MacOS\BSD等系统上使用,完全免费无广告。Windows版解压…...
STM32 CubeMX (第三步Freertos中断管理和软件定时)
STM32 CubeMX STM32 CubeMX (第三步Freertos中断管理和软件定时) STM32 CubeMX一、STM32 CubeMX设置时钟配置HAL时基选择TIM1(不要选择滴答定时器;滴答定时器留给OS系统做时基)使用STM32 CubeMX 库,配置Fre…...
Java虚拟机(JVM):堆溢出
一、概念 Java堆溢出(Java Heap Overflow)是指在Java程序中,当创建对象时,无法分配足够的内存空间来存储对象,导致堆内存溢出的情况。 Java堆是Java虚拟机中用于存储对象的一块内存区域。当程序创建对象时,…...
C语言,Linux,静态库编写方法,makefile与shell脚本的关系。
静态库编写: 编写.o文件gcc -c(小写) seqlist.c(需要和头文件、main.c文件在同一文件目录下) libs.a->去掉lib与.a剩下的为库的名称‘s’。 -ls是指库名为s。 -L库的路径。 makefile文件编写: CFLAGS-Wall -O2 -g -I ./inc/ LDFLAGS-L./lib/ -l…...
Php“牵手”淘宝商品详情页数据采集方法,淘宝API接口申请指南
淘宝天猫详情接口 API 是开放平台提供的一种 API 接口,它可以帮助开发者获取商品的详细信息,包括商品的标题、描述、图片等信息。在电商平台的开发中,详情接口API是非常常用的 API,因此本文将详细介绍详情接口 API 的使用。 一、…...
如何使用CSS实现一个全屏滚动效果(Fullpage Scroll)?
聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ 实现全屏滚动效果的CSS和JavaScript示例⭐ HTML 结构⭐ CSS 样式 (styles.css)⭐ JavaScript 代码 (script.js)⭐ 实现说明⭐ 写在最后 ⭐ 专栏简介 前端入门之旅:探索Web开发的奇妙世界 记得点击上方或者右侧链接订阅本专栏哦…...
Docker之Compose
目录 前言 1.1Docker Swarm与Docker Compose 1.1.1Docker Swarm 1.1.2Docker Compose 1.1.2.1 三层容器 编辑 二、YAML 2.1YAML概述 2.2注意事项 2.3Docker Compose 环境安装 2.3.1下载 三、Docker-Compose配置常用字段 四、Docker-compose常用命令 五、Docker…...
安装chromedriver 115,对应chrome版本115(经检验,116也可以使用)
目录 1. 查看Chrome浏览器的版本2. 找到对应的chromedriver3. 安装ChromeDriver 1. 查看Chrome浏览器的版本 点进这个网站查看:chrome://settings/help (真是的,上一秒还是115版本,更新后就是116版本了,好在chromedi…...
排序算法:插入排序
插入排序的思想非常简单,生活中有一个很常见的场景:在打扑克牌时,我们一边抓牌一边给扑克牌排序,每次摸一张牌,就将它插入手上已有的牌中合适的位置,逐渐完成整个排序。 插入排序有两种写法: 交…...
掌握AI助手的魔法工具:解密Prompt(提示)在AIGC时代的应用「上篇」
在当今的AIGC时代,我们面临着越来越多的人工智能技术和应用。其中一个引人注目的工具就是Prompt(提示)。它就像是一种魔法,可以让我们与AI助手进行更加互动和有针对性的对话。那么,让我们一起来了解一下Prompt…...
JMeter - 接口压力测试工具简单使用
【启动前配置】 启动JMeter前可以先配置语言和编码: 修改:E:\JMeter\apache-jmeter-5.5\bin\jmeter.properties文件中: 1.language=en # 指定语言 language=zh_CN 2.sampleresult.default.encoding=ISO-8859-1 # 指定编码 UTF-8 sampleresult.default.encoding=UTF-8 也…...
【C++入门到精通】C++入门 —— priority_queue(STL)优先队列
阅读导航 前言一、priority_queue简介1. 概念2. 特点 二、priority_queue使用1. 基本操作2. 底层结构 三、priority_queue模拟实现⭕ C代码⭕priority_queue中的仿函数 总结温馨提示 前言 ⭕文章绑定了VS平台下std::priority_queue的源码,大家可以下载了解一下&…...
静态代码扫描工具 Sonar 配置及使用
概览 Sonar 是一个用于代码质量管理的开放平台。通过插件机制,Sonar 可以集成不同的测试工具,代码分析工具,以及持续集成工具。与持续集成工具(例如 Hudson/Jenkins 等)不同,Sonar 并不是简单地把不同的代…...
docker 03(docker 容器的数据卷)
一、数据卷的概念和作用 删除后,数据也没了。 不能 数据卷 是宿主机中的一个目录或文件当容器目录和数据卷目录绑定后,对方的修改会立即同步一个数据卷可以被多个容器同时挂载 作用: 容器数据持久化 外部机器和容器间接通信 容器之间数据交换…...
【04】基础知识:typescript中的类
一、es5 对象 1、定义 类(对象) 原型链上的属性和方法会被多个实例共享。构造函数中的属性和方法不会。 // 自定义构造函数 function Person(name, age) {this.name namethis.age agethis.getInfo function() {console.log(${this.name} - ${this.…...
CCClippingNode:在游戏中实现遮罩效果、剪切效果,以涂抹糖霜为例,如何更好的实现涂抹效果,提高用户的游戏体验
CCClippingNode:在游戏中实现遮罩效果、剪切效果,以涂抹糖霜为例,如何更好的实现涂抹效果 设备/引擎:Mac(11.6)/cocos2d-x 开发工具:Xcode(13.0) 开发需求:…...
cuda gdb调试
如果cudaDeviceEnablePeerAccess函数不支持或不起作用,您仍然可以尝试其他方法来实现GPU之间的数据交换和通信。以下是一些替代方法: 通过主机内存进行数据传输: 如果GPU之间的数据交换不是非常频繁,您可以将数据从一个GPU复制到…...
【vim 学习系列文章 5 - cscope 过滤掉某些目录】
文章目录 cscope 过滤目录介绍 cscope 过滤目录介绍 第一步创建自己的cscope脚本~/.local/bin/cscope.sh,如下: function my_cscope() {CODE_PATHpwdecho "$CODE_PATH"echo "start cscope...."if [ ! -f "$CODE_PATH/cscope.…...
实验三 HBase1.2.6安装及配置
系列文章目录 文章目录 系列文章目录前言一、HBase1.2.6的安装二、HBase1.2.6的配置2.1 单机模式配置2.2 伪分布式模式配置 总结参考 前言 在安装HBase1.2.6之前,需要安装好hadoop2.7.6。 本篇文章参考:HBase2.2.2安装和编程实践指南 一、HBase1.2.6的安…...
LightDB sequence支持MAXVALUE最大值与Oracle相同
功能介绍 Oracle数据库在创建sequence的时候可以支持设置maxvalue 为9999999999999999999999999999,这样的SQL在LightDB23.3版本之前都是执行失败的。为了方便Oracle用户迁移到LightDB上,在LightDB23.3版本上,增加了sequence支持maxvalue设置…...
二、Kafka快速入门
目录 2.1 安装部署1、【单机部署】2、【集群部署】 2.2 Kafka命令行操作1、查看topic相关命令参数2、查看当前kafka服务器中的所有Topic3、创建 first topic4、查看 first 主题的详情5、修改分区数(注意:分区数只能增加,不能减少)…...
消息中间件-kafka实战-第五章-kafka重复消费、顺序消费及死信队列
目录 一、参考二、路由规则(分片规则)三、触发重复消费的场景场景一:触发rebalance问题描述可能原因实际影响参数在kafka0.10.1 之前:在kafka0.10.1之后:解决方案 场景二:服务宕机可能原因解决方案 消息幂等性 四、kaf…...
python爬虫9:实战2
python爬虫9:实战2 前言 python实现网络爬虫非常简单,只需要掌握一定的基础知识和一定的库使用技巧即可。本系列目标旨在梳理相关知识点,方便以后复习。 申明 本系列所涉及的代码仅用于个人研究与讨论,并不会对网站产生不好…...
从业务层的代码出发,去排查通用框架代码崩溃的问题
目录 1、问题说明 1.1、Release下崩溃,Debug下很难复现 1.2、用Windbg打开dump文件,发现崩溃在通用的框架代码中 2、进一步分析 2.1、使用IDA查看汇编代码尝试寻找崩溃的线索 2.2、在Windbg中查看相关变量的值 2.3、查看最近代码的修改记录&#…...
LLM预训练大型语言模型Pre-training large language models
在上一个视频中,您被介绍到了生成性AI项目的生命周期。 如您所见,在您开始启动您的生成性AI应用的有趣部分之前,有几个步骤需要完成。一旦您确定了您的用例范围,并确定了您需要LLM在您的应用程序中的工作方式,您的下…...
[Machine Learning] 损失函数和优化过程
文章目录 机器学习算法的目的是找到一个假设来拟合数据。这通过一个优化过程来实现,该过程从预定义的 hypothesis class(假设类)中选择一个假设来最小化目标函数。具体地说,我们想找到 arg min h ∈ H 1 n ∑ i 1 n ℓ ( X i…...
serialVersionUID 有何用途?如果没定义会有什么问题?
序列化是将对象的状态信息转换为可存储或传输的形式的过程。我们都知道,Java 对象是保持在 JVM 的堆内存中的,也就是说,如果 JVM 堆不存在了,那么对象也就跟着消失了。 而序列化提供了一种方案,可以让你在即使 JVM 停机…...
编写网站程序/徐州seo网站推广
Missing ISO 9660 imageThe installer has tried to mount image # 1. but cannot find it on the hard drive.Please copy this image to the drive and click Retry. Click Exit to abort the installation. 这个是安装过程中你没有把iso镜像拷贝到你的U盘所导致!…...
松原网站制作/兰州seo外包公司
1.PorterDuff.Mode.CLEAR 所绘制不会提交到画布上。2.PorterDuff.Mode.SRC 显示上层绘制图片3.PorterDuff.Mode.DST 显示下层绘制图片4.PorterDuff.Mode.SRC_OVER 正常绘制显示,上下层绘制叠盖。5.PorterDuff.Mode.DST_OVER 上下层都显示。下层居上显示。6.PorterDu…...
哪里可学做网站/竞价托管推广代运营
Oracle生成流水号(SJBM_20180201_000001) 隔天重置--MYBatis集成Oracle生成流水号(SJBM_20180201_000001) 隔天重置--MYBatis集成调用使用PLSQL创建函数,存储过程动态生成流水号1.建立关联表TB_DPS_FLOW_NOcreate table TB_DPS_FLOW_NO(type_name VARCHAR2(100),--…...
佛山技术支持 骏域网站建设/网络营销pdf
一、打开程序。 先介绍 System.Diagnostics.Process类:用来启动和停止进程的。 1、 Process pr new Process();//声明一个进程类对象process.StartInfo.FileName "C:\\Keil_v5\\UV4\\UV4.exe";process.Start(); 2、还可以简单点:Process的…...
网页设计代码模板免费/北京官网优化公司
Hibernate是一个非常著名的的对象--关系映射工具,本文使用的是Hibernate3.6的版本。本文通过建立一个工程,来引导大家学习hibernate,对hibernate有个认识。有些代码、概念不清楚没关系,后文会慢慢的介绍。文中也有大量的注释&…...
买电脑的怎么下wordpress/查询关键词排名软件
通过基因ID文件从fasta文件中提取特定的基因序列Extract sequence with header from a fasta file with specific ID given in another file最近在进行一些转录组数据的挖掘和分析实验,在实际的数据处理中,往往需要对目标序列文件进行基于一定条件的筛选…...