synchronized、volatile底层原理详解

2023-11-12

文章目录

一、synchronized

原文:

https://www.cnblogs.com/aspirant/p/11470858.html

https://zhuanlan.zhihu.com/p/29866981

synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的, 监视器锁本质又是依赖于底层的操作系统的mutex lock(互斥锁)来实现的。

synchronized的锁存在于对象头中, jvm基于进入和退出monitor实现同步, 是使用monitorenter指令和monitorexit指令实现的。monitorenter指令在编译后插入到同步代码块的开始位置, monitorexit指令插入到同步代码块的结束位置和异常处。任何对象都和一个monitor关联, 当一个monitor被持有后, 它将处于锁定状态

在执行monitorenter指令时, 首先要尝试获取对象的锁, 如果这个对象没有被锁定, 或者当前线程已经持有了这个对象的锁, 那么锁的计数器加1; 当执行monitorexit指令时, 锁的计数器减1, 当计数器减少到0时, 锁就被释放了; 如果获取对象锁失败, 则进入到同步阻塞状态。

反编译可以看出, 同步代码块是通过monitorenter指令和monitorexit指令实现的;

同步方法是, 常量池中多了ACC_SYNCHRONIZED 标志符实现的, 当然也可以使用monitorenter和monitorexit实现。

1、监视器Monitor

monitor结构及原理

应该是升级为重量级才会有下面这个monitor吧

与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁

(这里已经升级重量级锁了)也就是通常说Synchronized的对象锁,MarkWord锁标识位为10,其中指针指向的是Monitor对象的起始地址。在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的):

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; // 记录个数(计数器, 实现可重入功能)
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL; // 指向持有ObjectMonitor对象的线程
    _WaitSet      = NULL; // 处于wait状态的线程,会被加入到_WaitSet (对应生命周期中的等待阻塞, 即调用wait()方法)
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; // 处于等待锁block状态的线程,会被加入到该列表(对应同步阻塞, 即争抢不到锁)
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
}

ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象 ),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时:

  1. 首先会进入 _EntryList 集合,当线程获取到对象的monitor后,进入 _Owner区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1;
  2. 若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒;
  3. 若当前线程执行完毕,也将释放monitor(锁)并复位count的值,以便其他线程进入获取monitor(锁);

同时, Monitor对象存在于每个Java对象的对象头Mark Word中(存储的指针的指向),Synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时notify/notifyAll/wait等方法会使用到Monitor锁对象,所以必须在同步代码块中使用。

监视器Monitor有两种同步方式:互斥与协作

互斥: 即对共享变量的互斥数据访问

同步: 即Wait_Set等待队列, 对应java中的wait()、notify()

如上图所示,一个线程通过1号门进入Entry Set(入口区),如果在入口区没有线程等待,那么这个线程就会获取监视器成为监视器的Owner,然后执行监视区域的代码。如果在入口区中有其它线程在等待,那么新来的线程也会和这些线程一起等待。线程在持有监视器的过程中,有两个选择,一个是正常执行监视器区域的代码,释放监视器,通过5号门退出监视器;还有可能等待某个条件的出现,于是它会通过3号门到Wait Set(等待区)休息,直到相应的条件满足后再通过4号门进入重新获取监视器再执行。

注意:

当一个线程释放监视器时,在入口区和等待区的等待线程都会去竞争监视器,如果入口区的线程赢了,会从2号门进入;如果等待区的线程赢了会从4号门进入。只有通过3号门才能进入等待区,在等待区中的线程只有通过4号门才能退出等待区 //TODO 不能进入锁池?,也就是说一个线程只有在持有监视器时才能执行wait操作,处于等待的线程只有再次获得监视器才能退出等待状态。

2、Mutex lock

监视器锁(Monitor)本质是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。

互斥锁:用于保护临界区,确保同一时间只有一个线程访问数据。对共享资源的访问,先对互斥量进行加锁,如果互斥量已经上锁,调用线程会阻塞,直到互斥量被解锁。在完成了对共享资源的访问后,要对互斥量进行解锁。

mutex的工作方式

  1. 申请mutex
  2. 如果成功,则持有该mutex
  3. 如果失败,则进行spin自旋. spin的过程就是在线等待mutex, 不断发起mutex gets, 直到获得mutex或者达到spin_count限制为止
  4. 依据工作模式的不同选择yiled还是sleep
  5. 若达到sleep限制或者被主动唤醒或者完成yield, 则重复1)~4)步,直到获得为止

synchronized与java.util.concurrent包中的ReentrantLock相比, 由于JDK1.6中加入了优化, 性能基本持平, 只不过ReentrantLock功能更加丰富, 在实现需求的情况下, 优先选择synchronized。

3、java对象头

java对象布局

  1. 实例数据:存放类的属性数据信息,包括父类的属性信息;大小由成员变量的大小决定, 比如: byte和boolan1个字节, short和char两个字节, int和float是4个字节, long和double是8个字节, reference是4个字节。
  2. 对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐;
  3. 对象头:Java对象头一般占有2个机器码(Mark Word和类型指针各一个机器码)(在32位虚拟机中,1个机器码等于4字节,也就是32bit,在64位虚拟机中,1个机器码是8个字节,也就是64bit),但是 如果对象是数组类型,则需要3个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。

mark word里存储的数据随锁的标志位的变化而变化, 以32位jdk为例:

4、无锁、偏向锁、轻量级锁、重量级锁

synchronized是通过对象内部的monitor实现的, monitor本质是依赖于底层的操作系统的mutex lock(互斥锁)实现, 操作系统线程之间切换需要由用户态转换到内核态, 成本较高, 所以称为重量级锁。

JDK1.6中为了较少获得锁和释放锁性能的消耗, 引入了偏向锁、轻量级锁, 锁一共分为4种状态, 且只能升级不能降级:

由低到高: 无锁、偏向锁、轻量级锁、重量级锁

5、偏向锁

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。

引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。

获取偏向锁过程

  • (1)访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态
  • (2)如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤(5),否则进入步骤(3)。
  • (3)如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行(5);如果竞争失败,执行(4)。
  • (4)如果CAS获取偏向锁失败,则表示有竞争(CAS获取偏向锁失败说明至少有过其他线程曾经获得过偏向锁,因为线程不会主动去释放偏向锁)。当到达全局安全点(safepoint)时,会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着(因为可能持有偏向锁的线程已经执行完毕,但是该线程并不会主动去释放偏向锁),如果线程不处于活动状态,则将对象头设置成无锁状态(标志位为“01”),然后重新偏向新的线程;如果线程仍然活着,撤销偏向锁后升级到轻量级锁状态(标志位为“00”),此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁
  • (5)执行同步代码。

第4步中, 如果当前持有偏向锁的过程已经执行完代码, 标志位会职位(01: 无锁、偏向锁); 当前持有偏向锁的线程没有执行完, 锁标志位置为00, 表示轻量级锁, 竞争的线程自旋等待, 持有偏向锁的线程继续执行完。

撤销偏向锁的过程

如上步骤(4)。偏向锁使用了一种等到竞争出现才释放偏向锁的机制:偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

关闭偏向锁

偏向锁在Java 6和Java 7里是默认启用的。由于偏向锁是为了在只有一个线程执行同步块时提高性能,如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。

6、轻量级锁

轻量级锁是为了在线程近乎交替执行同步代码时提高性能。

Lock Record结构:

Lock Record 描述
Owner 初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL;
EntryQ 关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程;
RcThis 表示blocked或waiting在该monitor record上的所有线程的个数;
Nest 用来实现 重入锁的计数;
HashCode 保存从对象头拷贝过来的HashCode值(可能还包含GC age)。
Candidate 用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值0表示没有需要唤醒的线程1表示要唤醒一个继任线程来竞争锁。

(1)在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。这时候线程堆栈与对象头的状态如下图所示。

  • (2)拷贝对象头中的Mark Word复制到锁记录中。

  • (3)拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤(4),否则执行步骤(5)。

  • (4)如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如下图所示。

  • (5)如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,若当前只有一个等待线程,则可通过自旋稍微等待一下,可能另一个线程很快就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。

轻量级锁的解锁过程:

  • (1)通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word。
  • (2)如果替换成功,整个同步过程就完成了。
  • (3)如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程。

为什么升级为轻量锁时要把对象头里的Mark Word复制到线程栈的锁记录中呢?

因为在申请对象锁时 需要以该值作为CAS的比较条件,同时在升级到重量级锁的时候,能通过这个比较判定是否在持有锁的过程中此锁被其他线程申请过,如果被其他线程申请了,则在释放锁的时候要唤醒被挂起的线程。

为什么会尝试CAS不成功以及什么情况下会不成功?

CAS本身是不带锁机制的,其是通过比较而来。假设如下场景:线程A和线程B都在对象头里的锁标识为无锁状态进入,那么如线程A先更新对象头为其锁记录指针成功之后,线程B再用CAS去更新,就会发现此时的对象头已经不是其操作前的对象HashCode了,所以CAS会失败。也就是说,只有两个线程并发申请锁的时候会发生CAS失败。

然后线程B进行CAS自旋,等待对象头的锁标识重新变回无锁状态或对象头内容等于对象HashCode(因为这是线程B做CAS操作前的值),这也就意味着线程A执行结束(参见后面轻量级锁的撤销,只有线程A执行完毕撤销锁了才会重置对象头),此时线程B的CAS操作终于成功了,于是线程B获得了锁以及执行同步代码的权限。如果线程A的执行时间较长,线程B经过若干次CAS时钟没有成功,则锁膨胀为重量级锁,即线程B被挂起阻塞、等待重新调度。

7、重量级锁

如上轻量级锁的加锁过程步骤(5),轻量级锁所适应的场景是线程近乎交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。Mark Word的锁标记位更新为10,Mark Word指向互斥量(重量级锁)

Synchronized的重量级锁是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。

8 、偏向锁、轻量级锁、重量级锁之间转换

偏向所锁,轻量级锁都是乐观锁,重量级锁是悲观锁。

  • 一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。
  • 一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了。检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程。如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的),此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁;如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。
  • 轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数或者一个线程在持有锁,一个在自旋,又有第三个来访时轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。

9、其他锁优化

锁消除

锁消除即删除不必要的加锁操作。虚拟机即时编辑器在运行时,对一些“代码上要求同步,但是被检测到不可能存在共享数据竞争”的锁进行消除。

根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必要加锁。

锁粗化

如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。

如果虚拟机检测到有一串零碎的操作都是对同一对象的加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。

10 、自旋锁与自适应自旋锁

  • 引入自旋锁的原因: 互斥同步对性能最大的影响是阻塞的实现,因为挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性能带来很大的压力。同时虚拟机的开发团队也注意到在许多应用上面,共享数据的锁定状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。
  • 自旋锁: 让该线程执行一段无意义的忙循环(自旋)等待一段时间,不会被立即挂起(自旋不放弃处理器的执行时间),看持有锁的线程是否会很快释放锁。自旋锁在JDK 1.4.2中引入,默认关闭,但是可以使用-XX:+UseSpinning开开启;在JDK1.6中默认开启
  • 自旋锁的缺点: 自旋等待不能替代阻塞,虽然它可以避免线程切换带来的开销,但是它占用了处理器的时间。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好;反之,自旋的线程就会白白消耗掉处理器的资源,它不会做任何有意义的工作,这样反而会带来性能上的浪费。所以说,自旋等待的时间(自旋的次数)必须要有一个限度,例如让其循环10次,如果自旋超过了定义的时间仍然没有获取到锁,则应该被挂起(进入阻塞状态)。通过参数-XX:PreBlockSpin可以调整自旋次数,默认的自旋次数为10。
  • 自适应的自旋锁: JDK1.6引入自适应的自旋锁,自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定:如果在同一个锁的对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。简单来说,就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。
  • 自旋锁使用场景: 从轻量级锁获取的流程中我们知道,当线程在获取轻量级锁的过程中执行CAS操作失败时,是要通过自旋来获取重量级锁的。(见前面“轻量级锁”)

11、锁优劣

synchronized总结

  • synchronized特点: 保证内存可见性、操作原子性

  • synchronized影响性能的原因

    • 1、加锁解锁操作需要额外操作;
    • 2、互斥同步对性能最大的影响是阻塞的实现,因为阻塞涉及到的挂起线程和恢复线程的操作都需要转入内核态中完成(用户态与内核态的切换的性能代价是比较大的)
  • synchronized锁:对象头中的Mark Word根据锁标志位的不同而被复用

    • 偏向锁:在只有一个线程执行同步块时提高性能。Mark Word存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单比较ThreadID。特点:只有等到线程竞争出现才释放偏向锁,持有偏向锁的线程不会主动释放偏向锁。之后的线程竞争偏向锁,会先检查持有偏向锁的线程是否存活,如果不存货,则对象变为无锁状态,重新偏向;如果仍存活,则偏向锁升级为轻量级锁,此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁
    • 轻量级锁:在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间尝试拷贝锁对象目前的Mark Word到栈帧的Lock Record,若拷贝成功:虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向对象的Mark Word若拷贝失败:若当前只有一个等待线程,则可通过自旋稍微等待一下,可能持有轻量级锁的线程很快就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁
    • 重量级锁:指向互斥量(mutex),底层通过操作系统的mutex lock实现。等待锁的线程会被阻塞,由于Linux下Java线程与操作系统内核态线程一一映射,所以涉及到用户态和内核态的切换、操作系统内核态中的线程的阻塞和恢复。

二、volatile

转载原文: https://zhuanlan.zhihu.com/p/29868853

1、内存可见性

线程对共享变量修改的可见性, 当一个线程修改了共享变量的值, 另一个线程马上就可以看到。

volatile使用lock前缀的指令禁止线程本地内存缓存, 保证不同线程之间的内存可见性。

Java代码如下:

Singleton volatile instance = new Singleton(); // instance是volatile变量 

转变成汇编代码,如下:

0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp); 

为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据会立即写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。

2、volatile的内存语义:

  • volatile写的内存语义:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存
  • volatile读的内存语义:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

3、volatile写 - 读的内存语义:

  • 线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所做修改的)消息。
  • 线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。
  • 线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。

4、volatile禁止指令重排序语义的实现:

内存屏障

重排序可能会导致多线程程序出现内存可见性问题。对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers,Intel称之为Memory Fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序。通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。

为了保证内存可见性,Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。

StoreLoad Barriers是一个“全能型”的屏障,它同时具有其他3个屏障的效果。现代的多处理器大多支持该屏障(其他类型的屏障不一定被所有处理器支持)。执行该屏障开销会很昂贵,因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中(Buffer Fully Flush)。

JMM针对编译器制定volatile重排序规则表:

  • 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
  • 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
  • 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序

为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

下面是基于保守策略的JMM内存屏障插入策略:

  • 在每个volatile写操作的前面插入一个StoreStore屏障。
  • 在每个volatile写操作的后面插入一个StoreLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadStore屏障。

从编译器重排序规则和处理器内存屏障插入策略来看,只要volatile变量与普通变量之间的重排序可能会破坏volatile的内存语义(内存可见性),这种重排序就会被编译器重排序规则和处理器内存屏障插入策略禁止。

5、操作原子性

对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性,因为本质上volatile++是读、写两次操作。

对于复合操作,可以:

  • 同步块技术(锁)
  • Java concurrent包(原子操作类等)

volatile总结

volatile特点:

  • 通过使用Lock前缀的指令禁止变量在线程工作内存中缓存来保证volatile变量的内存可见性、通过插入内存屏障禁止会影响变量内存可见性的指令重排序
  • 对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

synchronized、volatile底层原理详解 的相关文章

  • ScheduledThreadPoolExecutor如何在特定时间运行任务?

    特别是 它是否像这样在内部实现了 while true 循环 while System currentTimeMillis lt timeToRunTask Thread sleep 1000 doTask From http grepco
  • 如何使用 Maven Failsafe 插件运行 JUnit 5 集成测试?

    当我运行命令时 Maven Failsafe 插件找不到我的 JUnit 5 集成测试mvn clean failsafe integration test 尽管它可以找到文件 我有junit jupiter api and junit j
  • Java 读取大文本文件时出现 OutOfMemoryError

    我是 Java 新手 正在读取非常大的文件 需要一些帮助来理解问题并解决它 我们有一些遗留代码 必须对其进行优化才能正常运行 文件大小仅在 10mb 到 10gb 之间变化 只有当文件开始大小超过 800mb 时才会出现启动问题 Input
  • GET 请求的 Spring 注解

    这两种spring GET方法有什么区别呢 哪一种是首选方法 Component Scope request Path public class TestComponent GET Path hello public String prin
  • Java - JPanel 内有边距和 JTextArea

    我想创建这样的东西 主面板有其边距 x 并且 TextArea 位于该面板的中心 几乎填满了面板 底部是另一个具有自定义尺寸 高度 y 的面板 可以使用某些快捷方式将其切换为可见和不可见 底部面板有 FlowLayout 和几个元素 问题是
  • java.lang.Object的hashCode具体使用的算法是什么

    中使用的算法是什么JVM实施java lang Object的隐含的hashCode 方法 OpenJDK or Oracle JDK答案中首选 它依赖于实现 并且在很大程度上 该算法是entirely取决于实施 只要它是一致的 但是 根据
  • 从 HttpClient 3 转换为 4

    我已经成功地对所有内容进行了更改 但以下内容除外 HttpClient client HttpPost method client new DefaultHttpClient method new HttpPost url InputStr
  • Android 解析 JSON 卡在 get 任务上

    我正在尝试解析一些 JSON 数据 我的代码工作了一段时间 我不确定我改变了什么突然破坏了代码 当我运行代码时 我没有收到任何运行时错误或警告 我创建一个新的 AsyncTask 并执行它 当我打电话时 get 在这个新任务中 调试器在此行
  • 合并两个地图的最佳实践是什么

    如何将新地图添加到现有地图 地图具有相同的类型Map
  • 多线程——更快的方法?

    我有一堂有吸气剂的课程getInt 和一个二传手setInt 在某个领域 比如说领域 Integer Int 一个类的一个对象 比如说SomeClass The setInt 这里是同步的 getInt isn t 我正在更新的值Int来自
  • 改变 Java 中凯撒移位的方向

    用户可以通过选择 1 向左或 2 向右移动字母来选择向左或向右移动 左边工作正常 右边不行 现在它显示了完全相同的循环 但我已经改变了所有 and 以不同的方式进行标记 最终我总是得到奇怪的字符 如何让程序将字符向相反方向移动 如果用户输入
  • Java和手动执行finalize

    如果我打电话finalize 在我的程序代码中的一个对象上 JVM当垃圾收集器处理这个对象时仍然再次运行该方法吗 这是一个大概的例子 MyObject m new MyObject m finalize m null System gc 是
  • Java:一个函数有多种返回类型...可以使用泛型吗?

    为了简单起见 我有一些程序 如下所示 public String fetchValueAsString String key public DateTime fetchValueAsDateTime String key 我想要类似的东西
  • android 中的 java.net.URL ..新手问题

    我是java新手 正在尝试android开发 以下代码生成 malformedURLException 有人可以帮助我识别异常吗 任何提示都会非常有帮助 package com example helloandroid import and
  • 如何清理 Runtime.exec() 中使用的用户输入?

    我需要通过命令行调用自定义脚本 这些脚本需要很少的参数并在 Linux 机器上调用 当前版本容易出现各种shell注入 如何清理用户给出的参数 参数包括登录名和路径 Unix 或 Windows 路径 用户应该能够输入任何可能的路径 该路径
  • 为什么我不能将 Collection 转换为 Collection>

    问题的关键是 为什么这会导致编译时错误 List
  • Jackson 反序列化相当于 @JsonUnwrapped 吗?

    假设我有以下课程 public class Parent public int age JsonUnwrapped public Name name 生成 JSON age 18 first Joey last Sixpack 我如何将其反
  • Android Google 地图无法在当前主题中找到样式“mapViewStyle”

    添加谷歌地图视图时 我扩展了MapView 使用xml编辑器将其添加到活动中 并将我的谷歌地图api密钥手动添加到布局xml文件中 我的权限在清单文件中允许互联网 我想知道的是 在 xml 编辑器中 我收到错误 无法在当前主题中找到样式 m
  • 使用 AmazonSNSClient 发送短信时的授权

    aws 官方文档如何发送短信 http docs aws amazon com sns latest dg sms publish to phone html使用 java 中的 aws SDK 非常简单 但是 当发送如底部示例所示的消息时
  • 将数组值导出到 csv 文件 java

    我只需要帮助将数组元素导出到 csv 文件 我不知道我的代码有什么问题 任何帮助将不胜感激 谢谢 for int index 0 index lt cols length index FileWriter fw new FileWriter

随机推荐

  • alive workers 数量为0的解决方案

    因为hostname显示的主机名与 etc hosts中的主机名不相同 所以都会导致无法连接slave 将集群的主机均实现hostname与 etc hosts名称一样 就解决了问题 如下所示
  • 捕获原点

    一 什么是捕获 即当某一种信号触发时 GTS 运动控制器能准确记录触发时刻轴的位置信息 二 捕获的方式 GTS 控制卡提供四种捕获方式 Home 捕获 Index 捕获 探针 Probe 捕获和 HSIO 捕获 1 Home捕获模式 GTS
  • vue3中使用第三方插件mitt实现任意组件通讯

    vue3中使用第三方插件mitt实现任意组件通讯 组件通讯是vue3组合式开发的核心之一 现在我在写代码时 一个组件的代码超过了200行 基本都会拆分组件 组件拆分后 组件之间的通讯就很重要 总结了一下 目前有这么几种组件通讯类型 父子通讯
  • Arduino基于ESP8266模块的TCP透传功能使用TCP透传协议连接移动onenet

    一 硬件资源 1 Arduion UNO R3 2 ESP8266WIFI模块 二 需要提前明确的知识点 1 ESP8266模块具有TCP透传功能 通过AT指令可以使得WIFI模块连接至相应的服务器 2 onenet具有多协议接入方式 例如
  • 网络流(入门)-概念

    相关概念介绍 这里的相关概念引用的是yxc大佬的讲解 在这里特别感谢yxc大佬的算法课 让我入了算法竞赛的门 1 1 流网络 G V E 特点 是一个有向图 且可以有环 不考虑反向边 即使有反向边 也可以通过加点来把一条反向边 变成两条单向
  • Candence学习篇(6)使用allegro绘制元器件的PCB封装

    文章目录 前言 一 确定引脚坐标位置 二 新建封装 2 1设置封装的大小 2 2 设置焊盘路径 三 绘制PCB封装 3 1参数设置 3 2放置边框矩形 3 3放置装配层 放置丝印层和1脚指示原点 总结 前言 前面我们讲了 Candence学
  • 一只兔子每三个月生兔子JAVA,兔子生兔子问题

    关于兔子生兔子的算法详解 有一对兔子 从出生后第3个月起每个月都生一对兔子 小兔 子长到第三个月后每个月又生一对兔子 假如兔子都不死 问每个月的兔子总数为多少 分析 第1个月 1对 第2个月 1对 第3个月 原来的1对 新生1对 2对 第4
  • 指针在函数中的传递,搞懂这两幅图指针基础就过关了

    a本身即是char 类型 所以 a是char 类型 strcpy 中里面的参数是地址 adf 前面应该要加 吧 printf输出的是数组 所以用a
  • Git安装操作流程(超超超级详细)

    一 前言 被迫投向程序媛的行列 一切都要白手起家 接下来就以初学者的视角手把手记录 git 教程 由于我体质特殊 过手的普通操作也总能有各类bug 因此教程也会不定期更新我的bug们 二 Git 下载及安装 1 Git 安装 首先去 Git
  • html jwt权限控制,SpringBoot+SpringSecurity+JWT实RESTfulAPI权限控制

    在整合jwt之前 我们首先要在SpringBoot中整合security的模块 来实现基于security的授权控制 用过security的人都知道 它的功能无比的强大比shiro还要强大 但是今天我就介绍security中关于权限控制和是
  • Linux基础与实操_韩顺平mooc知识点笔记

    Linux 目录 Linux 一 介绍 1 1目录结构 二 实操 2 1远程登陆 2 2 vi和vim 2 2 1 三种模式 2 2 2 快捷键 2 3 关机 重启 2 5 用户管理 2 5 1 用户家目录 2 5 2 添加用户 2 5 3
  • java入门到进阶书单

    入门 1 2年 初级 Head First Java 主要讲设计模式 这个是设计思想方面的 我之所以觉得它应该最早学 就是觉得这个对今后你看jdk tomcat源码 看第三方项目源码 以及一些大数据中间源码有所帮助 另外也有一本书叫 大话设
  • Matlab-矩阵

    目录 一 矩阵的操作 1 创建矩阵 1 建立简单矩阵 2 建立特殊矩阵 3 希尔伯特 Hilbert 矩阵 4 托普利兹 Toeplitz 矩阵 5 0 1间均匀分布的随机矩阵 6 标准正态分布随机矩阵 7 魔方矩阵 8 帕斯卡矩阵 9 范
  • IT中文技术站十大网站收藏

    1 CSDN www csdn net CSDN Chinese Software Developer Network 创立于1999年 是中国的IT社区和服务平台 为中国的软件开发者和IT从业者提供知识传播 职业发展 软件开发等全生命周期
  • 六种线程状态详解

    1 线程状态概述 线程从创建到运行到结束是一个线程的生命周期 当线程被创建到结束过程中 不是一直处于运行状态的 下面来介绍一下线程从运行到结束所有的状态 线程状态 导致状态发生条件 NEW 新建 线程刚被创建 没有启动 也就是还没调用sta
  • LeetCode 841.钥匙和房间 - C++ - 小结

    钥匙和房间 有 N 个房间 开始时你位于 0 号房间 每个房间有不同的号码 0 1 2 N 1 并且房间里可能有一些钥匙能使你进入下一个房间 在形式上 对于每个房间 i 都有一个钥匙列表 rooms i 每个钥匙 rooms i j 由 0
  • python制作模型排放清单_四川省人为源大气污染物排放清单及特征

    大气污染源排放清单是污染源在一定时间跨度和空间区域内排放到大气中的各种污染物的数量列表 在支持空气质量管理决策的技术体系中 构建准确 完整和更新及时的大气污染物排放清单是识别污染来源的基础环节 也是制订污染控制策略的根本依据 同时也对大气污
  • 【论文笔记】Interventional Few-Shot Learning

    概述 目前流行的meta learning fine tuning等小样本学习方法都利用外部知识进行预训练 以使模型在小样本上有好的效果 而预训练知识很可能会成为影响样本特征与标签之间的关系的混杂因素 使模型得到错误的结果 本文从因果的角度
  • error while loading shared libraries: libssl.so.6

    执行 scripts mysql install db user mysql 安装mysql的时候报错 bin my print defaults error while loading shared libraries libssl so
  • synchronized、volatile底层原理详解

    文章目录 一 synchronized 1 监视器Monitor monitor结构及原理 监视器Monitor有两种同步方式 互斥与协作 2 Mutex lock mutex的工作方式 3 java对象头 java对象布局 4 无锁 偏向