不断的学习,我们才能不断的前进
一个好的程序员是那种过单行线马路都要往两边看的人

Volatile

volatile 关键字在并发编程中保证了共享变量的“可见性”。可见性是指当一个线程修改一个共享变量时,另外一个线程能够读到这个修改的值。Synchronized 关键字也可以保证可见性(使用锁),但是volatile 变量修饰符的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。

CPU术语定义

术语 英文单词 描述
内存屏障 memory barriers 一组处理器指令,用于实现对内存操作的顺序限制
缓冲行 cache line CPU高速缓存中可以分配的最小存储单元。处理器填写缓存行时会加重整个缓存行
原子操作 atomic operations 不可中断的一个或一系列操作
缓存行填充 cache line fill 当处理器识别到从内存中读取操作数是可缓存的,处理器读取整个高速缓存行到适当的缓存(L1,L2,L3 or all)
缓存命中 cache hit 如果高速缓存行填充操作的内存位置任然是下次处理器访问的地址时,处理器从缓存中读取操作数,而不是内存读取
写命中 write hit 当处理器将操作数写回到一个内存缓存的区域时,它首先会坚持这个缓存的内存地址是否在缓存行中,如果存在一个有效的缓存行,则处理器将这个操作数写回到缓存,而不是内存。
写缺失 write misses the cache 一个有效的缓存行被写入到不存在的内存区域

cpu

Bus 系统总线

如何保证可见性?

volatile关键字修饰的共享变量,转化成汇编语言后,会多出一行包含Lock指令的代码。这个指令会引发两件事情:

  • 将当前处理器缓存行的数据写回到系统内存
  • 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效
private volatile Object instance=new Singleton()

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

具体的实现

  1. Lock 前缀指令会引起处理器缓存写回到内存,
  2. 一个处理器的缓存写回到内存会导致其他处理器的缓存无效:处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致。如果通过嗅探一个处理器来检测其他处理器打算写内存地址,而这个地址处于共享状态,那么正在嗅探的处理器将使它的缓存行无效,在下次访问相同内存地址时,强制执行缓存行填充。

Synchronized

Java 对象头

JVM虚拟机的对象头一共分为两个部分:第一部分用来存储对象自身的运行时数据,比如哈希吗、GC分代年龄等,一般称为Mark Word; 第二部分是用于存储指向方法区对象类型数据的指针如果是数组对象,还会有额外一个部分用来存储数据长度。
Mark Word被设计成非固定的动态数据结构,假如一个32位的的hotspot虚拟机,Mark Word结构如下:
markword1

锁升级和比较

锁一共有四种状态,级别从低到高分别是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,这个几个状态会随着竞争逐渐升级。锁可以升级但不能降级。

偏向锁

在大多数情况下,锁不仅不存在多线程竞争,而且总是同一个线程多次获得,为了让线程获得锁的代价更低从而引入来偏向锁。当一个线程访问同步快获取锁时,会在对象头和栈帧中的锁记录里面存储偏向的线程ID,以后该线程进入和退出同步代码块时不需要进行CAS 操作来加锁和解锁。

偏向锁的目的是消除数据在无竞争的情况下的同步语句,进一步提高程序的运行性能。偏向锁,在无竞争的情况下,把整个同步都去掉,甚至CAS操作都不去做。偏向锁认为在任何时刻,只有一个线程访问同步代码块。

偏向锁加锁的过程
当收到monitorenter指令,进入到同步代码块时,会进行加锁操作;当对象第一次被线程获取的时候,虚拟机会把对象头Mark Word中的标志位设置为01,并且把偏向模式设置为1,表示进入偏向模式,同时使用CAS操作把获取到这个锁的线程ID记录在Mark Word中。如果CAS成功,则持有偏向锁的线程以后每次进入这个锁相关的同步快时,虚拟机不会进行任何同步操作,只需要进行线程ID对比。

如果另外一个线程尝试去获取这个锁的情况,偏向模式立马结束。然后检查原来持有锁的线程,是否存活,如果挂了,当前对象就撤销回到无锁状态,重新偏向新的线程;如果原来的线程依旧存活,则检查原来的线程持有锁的情况,如果依然需要持有偏向锁,则进行锁升级的过程,升级到轻量级锁。后序的操作就像指向轻量级锁那样执行。
偏向锁的撤销
偏向锁使用了一种等到锁竞争才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(这个时间点上没有正在执行的字节码,类似STW)。
首先会检测暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活跃状态,则将对象头设置成无锁状态;如果线程任然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向其他线程,要么恢复到无锁、活着进行锁升级,最后唤醒暂停的线程。

轻量级锁

设计的目的是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。轻量级锁认为在任何时刻,=只存在多线程交替的去获取锁的情况,不存在同时竞争的情况==。

轻量级锁加锁的工作过程:
当收到monitorenter指令,进入同步代码块时,会进行加锁操作;在代码即将进入同步代码快时,如果此同步对象没有被锁定(标志位为01),JVM虚拟机将在当前线程的栈帧中创建一个锁记录(Lock Record)的空间,其中displaced字段用于存储锁对象目前的Mark Word拷贝(Displaced Mark Word)
然后,虚拟机尝试使用CAS 操作把对象的Mark Word 高位替换为指向Lock Record 的指针。如果更新成功,代表这个线程拥有了这个对象的锁,并且Mard Word的锁标志位设置为00,表示处于轻量级锁的状态。
如果CAS更新操作失败了,那就意味至少存在一条线程与当前线程竞争获取该对象的锁。那么虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是,说明当前线程已经拥有了这个对象的锁,那么直接进入同步代码快。否则说明这个锁被其他线程占用,所以轻量级锁就不再有效,需要膨胀为重量级锁。

轻量级锁解锁的工作过程:
当收到monitorexist指令时,会进行解锁操作;解锁过程使用CAS来完成的,如果对象的Mark Word仍然指向当前线程栈帧里面的锁记录,那么就CAS操作把对象当前的Mark Word对象和线程中复制的Displaced Mark Word替换回来。如果替换成功,则解锁完成;如果替换失败,说明有其他线程有尝试获取过该锁,所以在释放锁的同时,唤醒被挂起的线程。

重量级锁

重量级锁指向的指针就是指向monitor对象(EntrySet、Owner、WaitSet),每一个java对象都和一个ObjectMonitor对象相关联,关联关系存储在该java对象的对象头里的Mark Word中。

在java中,每一个等待锁的线程都会被封装成ObjectWaiter对象,当多个线程同时访问一段同步代码时,首先会被扔进 EntryList 集合中,如果其中的某个线程获得了monitor对象,他将成为 owner,如果在它成为 owner之后又调用了wait方法,则他将释放获得的monitor对象,进入 WaitSet集合中等待被唤醒。

hashcode去哪里了
当对象进入偏向状态时,Mark Word存放hashcode的位置用来存放线程ID了,那么对象的哈希码去哪里了?
在Java对象的hashcode来自于Obejct类,是一致性哈希码,是强制不可改变的。通过在对象头中存储计算结果来保证第一次计算后,再次调用该方法获取到的哈希码值永远不会改变。因此当一个对象已经计算过一致性哈希码后,就再也不能进入偏向锁状态;而当对象正处于偏向锁状态时,如果收到计算一致性哈希码的请求,它的偏向状态会立马被撤销,并且锁会被膨胀为重量级锁在重量级锁有字段可以记录非加锁状态的Mark Word,其中有原来的哈希码

什么是Synchronized偏向锁?加锁流程?

当我们使用Synchronized向对象进行加锁时,如果对象的对象头Mark Word的二进制数中,对应的锁标志位是01,偏向模式是0,也就是无锁的状态,这个时候线程直接会通过CAS向Mark Word的高位内存储当前线程的线程ID;如果设置成功了,当前这把锁就是偏向当前线程的锁;如果设置失败,说明有其他线程尝试去获取锁,这个时候通过对象头Mark World获取之前持有锁的线程,检查它是否存活,如果失效,则当前对象撤回到无锁的状态,设置当前线程为偏向锁;如果之前的线程依旧存活,则需要进行锁升级,升级到轻量级锁。
偏向锁加锁成功后,就可以直接执行同步代码快了。

偏向锁有什么优势?

为什么要使用偏向锁呢?因为我们在写代码的时候,遇到并发情况下需要保证安全性的时候,就要使用Synchronized关键字,然而可能这块代码,可能一直只有一个线程来执行,直接使用偏向锁的话,只需要比较对象头的线程ID即可,减少了性能消耗。而轻量级锁要进行CAS操作。

偏向锁会主动释放么?为什么?

遇到monitorexit的时候,需要释放锁。JVM会将当前线程栈内与当前锁对象相关的记录全部获取到,然后释放掉最后一条锁记录;然后通过检查锁对象的markword,如果是偏向锁模式,则什么也不做,直接退出,也就是同步代码块执行完毕之后,当前对象仍然保留了偏向锁的线程ID
好处就是偏向锁退出时,仍然保留偏向的线程ID,下次这个线程再进入同步代码块时,只需要比较锁对象markword的线程ID,即可获取锁,性能消耗更低

偏向锁一定会提升性能吗
不一定,当只有一个线程进入到同步代码快的时候,是会提升性能的;但是如果如果有多个线程需要进入到同步代码快时,就会触发锁升级,升级到轻量级锁,这快会进行很多检查来保证锁的正确性,性能就会下降。

什么是Synchronized轻量级锁?

轻量级锁,是无法解决线程竞争的问题,并不提供线程互斥性。轻量级锁认为在任何时刻,只存在多线程交替的去获取锁的情况,不存在同时竞争的情况,因为在同步代码块这部分,大多数情况下是多线程交替的去执行。

轻量级锁和偏向锁有什么区别?

偏向锁的假设前提是这个锁只有一个指定的线程去获取。
轻量级锁的假定条件是多个线程交替去获取这个锁。

假设关闭了偏向锁,从无锁状态到轻量级锁的加锁流程?

遇到monitorenter指令的时候,jvm执行这个指令之前,会向当前线程栈里面插入一条锁记录,锁记录内的锁引用字段保存锁对象地址;然后会让当前锁对象生成一个无锁状态的mark word,也称作displaced mark word。然后把生成的无锁状态的displaced mark word值保存到当前锁记录的displaced字段内。最后使用CAS的方式去设置当前锁对象的markword值,修改为当前线程持有轻量级锁状态。如果是无锁状态,则一定会修改成功,然后就变成无锁到轻量级锁的过程。

轻量级锁状态时,锁重入,JVM如何处理?

当遇到monitorenter指令的时候,jvm执行这个指令之前,还是会向当前线程栈里面插入一条锁记录,锁记录内的锁引用字段指向当前锁对象;然后会让当前锁对象生成一个无锁状态的displaced mark word。然后把生成的无锁状态的displaced mark word值保存到当前锁记录的displaced字段内。最后使用CAS的方式去设置当前锁对象的markword值,因为当前线程已经持有锁来,所以这一步CAS就会失败,然后判断为什么失败,然后发现锁对象的mark word内锁持有者的线程是本身,然后把刚刚插入的锁记录的displaced字段的引用该为空就行了
锁重入次数是通过当前线程栈内 当前锁的锁记录数来判断的,也就是每重入一次锁,当前线程栈内就会插入一条对应的锁记录。

轻量级锁释放锁的流程?

遇到monitorexit的时候,需要释放锁。JVM会将当前线程栈内与当前锁对象相关的记录全部获取到,然后释放掉最后一条锁记录,然后把锁记录的锁字段设置为null。然后通过检查锁字段的displaced字段,是否有值,如果没有值,说明这条锁记录是锁重入存放的。然后将这一条锁记录进行释放即可,就是把锁的引用字段设置为null 就可以完成一次锁的退出。
最后一次执行锁退出的逻辑?
也是将当前线程栈内的锁记录的引用字段设置为null,然后检查锁记录的displaced字段,该字段存放来第一次加锁放入的displaced mark word,这个displaced mark word就代表第一次加锁之前的无锁状态。然后把displaced mark word通过CAS 的方式 设置回当前锁对象的mark word。设置成功后就完全释放来锁;如果失败的话,说明当前锁已经升级到重量级锁 或者正处于锁膨胀中,需要进行重量级锁释放。

偏向锁偏向线程A,那么线程B请求获得锁,会做哪些事情?

当遇到monitorenter指令的时候,jvm执行这个指令之前,还是会向当前线程栈里面插入一条锁记录,锁记录内的锁引用字段指向当前锁对象;然后检查锁的状态,发现当前锁处于偏向锁状态,并且偏向线程也不是当前线程。如果之前的偏向线程没有存活,然后当前线程会提交一个撤销偏向锁的任务,把当前锁的状态设置为无锁状态;如果之前的偏向线程依然存活,则需要计算偏向线程是否还处于同步代码块之内,通过遍历线程栈内的锁记录来判断,如果栈内有一条锁记录的锁引用字段指向当前锁对象,那么说明偏向线程仍然处于同步代码快之内,否则表示线程已经跳出同步代码块。
如果偏向线程处于同步代码快之外,就直接将锁对象的mark word设置为无锁状态;
如果偏向线程处于同步代码快之内,需要将偏向锁升级为轻量级锁,首先遍历偏向线程的栈,找到锁记录指向当前锁对象的第一条记录,修改锁记录的displaced字段的数据为无锁状态的mark word,也就是displaced mark word,在锁释放的时候会使用它;然后在修改锁对象的markword为轻量级锁状态
当偏向线程处于同步代码快之内,需要进行锁升级为轻量级锁,然后外部线程还会继续自旋检查,它会检查出当前锁对象处于轻量级状态,而且同步代码块内有多个线程竞争访问,所以会再触发一次锁升级,由轻量级锁升级到重量级锁,利用重量级锁的互斥性来保证线程安全。

JVM safepoint 安全点?

出现这个safepoint状态的时候,所有的线程都处于阻塞状态,所有的线程都不能执行任务。此时只有VM线程在执行任务,可以执行一些特殊的任务,比如FULL GC、撤销偏向锁。
因为撤销偏向锁的过程会修改持有锁的线程的栈的数据,如果不在安全点内执行的话,会出现并发的问题。

JVM 的 VM线程?

重量级锁管程对象ObjectMonitor内部有哪些核心数据结构?

一个竞争队列、一个EntryList队列、一个阻塞等待队列WaitList、还有一个表示当前持有锁的线程。
竞争队列和EntryList队列 用来处理等待获取锁资源的线程。
阻塞队列WaitList用来处理持有锁线程调用锁对象的wait方法使用的。
重量级锁的实现和AQS类似,都是管程模型

重量级锁状态时,线程获取锁的流程是什么?

如果是重量级锁的话,mark word内保存的是管程对象内存位置 和 重量级锁状态。 抢占锁的线程到管程内去抢占锁,然后通过自旋的方式去尝试获取锁,也就是通过CAS把当前线程设置为持有锁的线程,设置成功的话,则说明抢占成功。如果失败的话,则会把自己封装成WaiterNode 结点,插入到竞争队列里面,去竞争锁。竞争失败就会会把自己挂起,通过unsafe.unpark,一直等到其他线程唤醒。

重量级锁状态时,线程完全释放锁时,会做什么事情?

释放锁的过程就是把当前持有锁的线程设置为null,然后去检查竞争队列和EntryList,如果有等待锁的线程的话,就会去唤醒线程抢占这把锁。

重量级锁是不是公平锁?

重量级锁是非公平锁,因为在锁释放,和唤醒等待队列线程这一段时间内,其他线程是会来竞争锁的。

什么情况下锁会膨胀为重量级锁?

有三种情况下,会进行锁膨胀:

  1. 当收到对象的一致性哈希码请求的时候,会进行锁膨胀,因为在偏向锁和轻量级锁的对象头Mark Word 字节码里面没有存储hashcode,而重量级锁里面有字段可以记录hashcode,所以会触发锁膨胀。
  2. 轻量级锁的同步代码块里面,存在多个线程同时竞争访问,这个时候就需要触发锁膨胀。
  3. 持有锁的线程调用wait()方法会导致膨胀,因为其他锁状态是没有管程对象存在的。

轻量级锁膨胀为重量级锁的流程?

当轻量级锁,存在锁竞争的时候,当前竞争线程,会获取锁失败,则进入锁膨胀的过程:
在锁膨胀过程,会判断当前锁是轻量级锁,接下来这个线程会获取一个空闲的管程对象,然后通过CAS修改锁对象状态为膨胀中,如果CAS设置失败,说明有其他线程正在膨胀 或者已经膨胀结束来,再次自旋获取即可。
如果CAS 操作成功,则当前线程需要执行锁升级操作,把管程对象的当前持有锁的线程设置为 原持有轻量级锁的线程,然后再将持有锁线程的第一条锁记录内存储的displacedMarkWord 保存到管程对象内,然后设置锁对象的mark word 状态为重量级锁,一个是重量级锁对象的内存位置,一个是重量级锁状态。然后其他线程来获取锁时,可以通过对象头mark word找到管程的位置。

Reference

浅谈Synchronized
深入分析synchronized的实现原理


目录