概述
锁的核心逻辑是当多工作单元在执行公共代码逻辑处理共享数据时保证数据正确性。Java的场景下的多工作单元是Thread 线程。代码逻辑为Java方法区中的方法。共享数据为Java对象中的数据变量。
synchronized为悲观锁,都是针对对象上锁,获取锁成功后,才能执行类中的方法。
Java类对象有俩种类型,所以加锁也只有两种类型:
一:Object对象实例,程序中new 出来的对象,大部分情况放在堆中。常见使用场景为普通方法上加synchronized或者锁对象。
二:Class对象实例,在类加载的时候会创建Class的对象。常见使用场景为静态方法上加synchronized。
工作原理
java中synchronized的逻辑是多线程锁竞争悲观锁。并且将锁的信息记录在对象头中的MarkWord。
java中synchronized会被解析成字节码的monitorenter和monitorexit语句。
synchronized现在共有三种锁,四种情况。分别为无锁、偏向锁、轻量级锁、重量级锁,其中无锁并不是一种锁。最早synchronized只有重量级锁,重量级锁调用内核态的mutex争强锁资源,效率较低。后来发现大部分场景都是同一线程获取对象锁,并不需要调用系统内核态,所以增加用户态的jvm内部的偏向锁和轻量级锁,即在jvm层面处理锁竞争逻辑,提升性能。
锁升级
锁升级的过程:无锁->偏向锁->轻量级锁->重量级锁。
无锁状态
对象新创建时为无锁状态。无锁时的MarkWord如下图所示:
偏向锁
简介
因为轻量级锁和重量级锁性能相对较低,并且大部分代码场景都是同一个线程重复进入synchronized代码块,所以无需使用轻量级锁和重量级锁,因此加入偏向锁。
偏向锁的逻辑为当第一个线程获取锁成功时,对象头中的markword记录线程id。同一线程再获取时锁时,只需要比较对象头中的线程id和获取锁的线程id是否相同,如果相同则直接成功获取锁。
加锁步骤
获取偏向锁步骤:
一:如果只有一个线程获取锁,则将MarkWord修改为偏向锁结构。修改的MarkWord内容如下:
1:将锁对象的锁标志位(lock)被改为01。
2:将偏向标志位(biased_lock)被改为1。
3:通过CAS将线程的ID和时间戳(Epoch)记录在锁对象的MarkWord中。如果变更失败,则说明已经有其他线程占用当前对象,则锁升级为轻量级锁。
轻量级锁,自旋锁
背景
当有多个线程需要同时操作一个方法时,因为需要保证只有一个线程执行。所以多个线程需要进行锁竞争。竞争成功的线程才能执行方法。
轻量级锁相比较重量级锁性能较快。因为轻量级锁是用户态处理逻辑,而重量级锁会触发系统内核态的锁竞争。所以当多个线程竞争锁时,优先使用用户态的锁处理抢占资源问题。
锁升级步骤
当第一个线程抢占锁成功时,对象头中的markword记录的信息为偏向锁。只要有第二个线程进行锁抢占操作,jvm认为有多个线程竞争,则升级为轻量级锁。升级过程为以下几步骤:
一:偏向锁撤销:撤销对象上的偏向锁,撤销偏向锁有以下四步骤。
(1)在一个安全点停止拥有锁的线程。
(2)遍历线程的栈帧,检查是否存在锁记录。如果存在锁记录,就需要清空锁记录,使其变成无锁状态,并修复锁记录指向的Mark Word,清除其线程ID。
(3)将当前锁升级成轻量级锁。
(4)唤醒当前线程。
二:轻量级锁抢占:每个线程的线程栈生成各自的锁记录(Lock Record),然后通过CAS将线程栈帧中锁记录(Lock Record)的地址替换到MarkWord中的锁记录指针(ptr_to_lock_record)。
Lock Record主要存储两部分内容
(1):Displaced Mark Word,对象中的Mark Word拷贝到线程栈中的 Displaced Mark Word,目的是当锁撤销时再将栈帧中的 Displaced Mark Word的内容拷贝回对象头中。
(2):owner,指向锁的对象。
第二步骤的轻量级锁抢占时,主要修改的数据内容如下:
对象Mark Word 部分:
(1):将锁对象的锁标志位(lock)被改为00,代表轻量级锁标志。
(2):将锁对象的MarkWord中信息改为锁记录(Lock Record)指针。
栈帧LockRecord部分:
(1):将LockRecord的Displaced Mark Word修改为MarkWord的信息。
(2):将owner存入锁对象的指针。
替换前的示例图如下图所示:
替换后的示例图如下图所示:
三:失败线程自旋:线程1抢占成功,则线程2通过CAS抢占将会失败,线程2自旋处理,自旋锁的名字由此得来。自旋锁长时间获取不到锁时满足升级条件后将会升级为重量级锁。
自旋升级条件有俩种类型:
a:普通自旋:
(1)自旋10次。
(2)等待线程数超过半数cpu个数。
b:自适应自旋:策略是动态调整的。核心理念只有一个,根据上一次自旋的时间和结果调整下次的时间。(jdk 1.7+)
重量级锁
背景
因为轻量级锁是通过用户态的JVM的线程自旋,并且自旋过程中会消耗cpu处理性能。所以当满足条件后会升级为重量级锁。
原理
早期的JDK版本只有重量级锁,hotspot实现重量级锁为内核态抢占mutex。因为mutex为队列,数量有限制,所以通过抢占队列元素实现的重量级锁。
总结
synchronized是JDK中用于加锁的关键字。属于悲观锁。多线程抢占markword中的头信息抢占锁资源。锁会根据抢占的线程的数量逐步升级。