如果一个线程获得了锁,即锁直接成为了锁的持有者,那么锁(其实就是临界资源对象)就进入了偏向模式,此时Mark Word的结果就会进入之前展示的偏向锁结构。
那么当该线程进再次请求该锁时,无需再做任何同步操作(不需要再像第一次获得该锁那样,进行较为复杂的操作),即获取锁的过程只需要检查Mark Word的锁标记位位偏向锁并且当前线程ID等于Mark Word的ThreadID即可,从而节省大量有关锁申请的操作。
看得有点懵,没关系,我会好好详细解释的。此处有关偏向锁的内存变化过程就两个,一个是第一次获得锁的过程,一个是后续获得该锁的过程。
接下来,我会结合图片,来详细阐述这两个过程的。
当一个线程通过Synchronized锁,出于需求,对共享资源进行独占操作时,就得试图向别的锁的竞争者宣誓锁的所有权。但是,此时由于该锁是第一次被占用,也不确定是否后面还有别的线程需要占有它(大多数情况下,锁不存在多线程竞争情况,总是由同一线程多次获得该锁),所以不会立马进入资源消耗较大的重量锁,轻量级锁,而是选择资源占用最少的偏向锁。为了向后面可能存在的锁竞争者线程证明该共享资源已被占用,该临界资源的Mark Word就会做出相应变化,标记该临界资源已被占用。具体Mark Word会变成如下形式:
含义 thread epoll age biased_lock lock示例 aaa...(23位bit) bb(2位bit) xxxx(4位bit) 1(1位bit ,具体值:1) 01(2位bit ,具体值:01)
这里我来说明一下其中各个字段的具体含义:
thread用于标识当前持有锁的线程(即在偏向锁状态下,表示当前该临界资源被哪个线程持有)
epoll:用于记录当前对象的mark word变为偏向结果的时间戳(即当前临界资源被持有的时间戳)
age:与无锁状态作用相同,无变化
biased_lock:值为1,表示当前mark word为偏向锁结构
lock:配合biased_lock共同表示当前mark word为偏向锁结果(至于为什么需要两个字段共同表示,一方面2bit无法表示4种结构,另一方面,最常用的偏向锁结果,利用1bit表示,既可以快速检验,又可以降低检验的资源消耗。需要的话,之后细说,或@我)
接下来就是第二个过程:锁的竞争者线程尝试获得锁,那么锁的竞争者线程会检测临界资源,或者说锁对象的mark word。如果是无锁状态,参照上一个过程。如果是偏向锁状态,就检测其thread是否为当前线程(锁的竞争者线程)的线程ID。如果是当前线程的线程ID,就会直接获得临界资源,不需要再次进行同步操作(即上一个过程提到的CAS操作)。
还看不懂,再引入一位大佬的流程解释:
偏向锁的加锁过程:
访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,确认为可偏向状态。
如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤5,否则进入步骤3。
如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行5;如果竞争失败,执行4。
如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。(撤销偏向锁的时候会导致stop the word)
执行同步代码。
PS:safepoint(没有任何字节码正在执行的时候):详见JVM GC相关,其会导致stop the world。
偏向锁的存在,极大降低了Syncronized在多数情况下的性能消耗。另外,偏向锁的持有线程运行完同步代码块后,不会解除偏向锁(即锁对象的Mark Word结构不会发生变化,其threadID也不会发生变化)
那么,如果偏向锁状态的mark word中的thread不是当前线程(锁的竞争者线程)的线程ID呢?
轻量级锁轻量级锁可能是由偏向锁升级而来的,也可能是由无锁状态直接升级而来(如通过JVM参数关闭了偏向锁)。
偏向锁运行在一个线程进入同步块的情况下,而当第二个线程加入锁竞争时,偏向锁就会升级轻量级锁。
如果JVM关闭了偏向锁,那么在一个线程进入同步块时,锁对象就会直接变为轻量级锁(即锁对象的Mark Word为偏向锁结构)。