从JVM的角度来看Java的多线程(3)

CAS操作需要3个参数,分别是内存位置V,旧的期望值A和新值B。CAS指令执行时,当且仅当V当前值符合旧值A时,处理器用新值B更新V的值,否则不执行更新。上述过程是一个原子操作。

轻量级锁加锁

假设现在开启了轻量级锁,当第一个线程要锁定对象时,该线程首先会在栈帧中建立Lock Record(锁记录)的空间,用于存储对象目前MarkWord的拷贝,然后虚拟机将使用CAS操作尝试将对象的MarkWord更新为指向线程锁记录的指针。如果操作成功,则该线程获得对象锁。如果失败,说明在该线程拷贝对象当前MarkWord之后,执行CAS操作之前,有其他线程获取了对象锁,我们最开始的假设“被加锁的代码不会发生并发”失效了。此时轻量级锁还不会直接膨胀为重量级锁,线程会自旋不停地重试CAS操作寄希望于锁的持有线程主动释放锁,在自旋一定次数后如果还是没有成功获得锁,那么轻量级锁要膨胀为重量级锁:之前成功获取了轻量级锁的那个��程现在依旧持有锁,只是换成了重量级锁,其他尝试获取锁的线程进入等待状态。

轻量级锁解锁

轻量级锁的解锁也是用CAS来操作,如果对象的MarkWord中依然是持有锁线程的锁记录指针,则CAS成功,把锁记录中的原MarkWord的拷贝复制回去,解锁完成;如果对象的MarkWord中保存的不再是持有锁线程的锁记录指针,说明在持有锁线程持有锁期间,这个轻量级锁已经因为其它线程并发获取膨胀为了重量级锁,因此线程在释放锁的同时,还要唤醒(notify)等待的线程。

偏向锁

根据轻量级锁的实现,我们知道虽然轻量级锁不支持“并发”,遇到“并发”就要膨胀为重量级锁,但是轻量级锁可以支持多个线程以串行的方式访问同一个加锁对象。比如A线程可以先获取对象o的轻量锁,然后A释放了轻量锁,这个时候B线程来获取o的轻量锁,是可以成功获取得,以这种方式可以一直串行下去。之所以能实现这种串行,是因为有一个释放锁的动作。那么假设有一个加锁的java方法,这个方法在运行的时候其实从始至终只有一个线程在调用,但是每次调用完却也要释放锁,下次调用还要重新获得锁。

那么我们能不能做一个假设:“假设加锁的代码从始至终就只有一个线程在调用,如果发现有多于一个线程调用,再膨胀成轻量级锁也不迟”。这个假设,就是偏向锁的核心思想。

核心实现

偏向锁的核心实现很简单:假设开启了偏向锁,当第一个线程尝试获得对象锁的时候,也会在栈帧中建立Lock Record锁记录,但是这个Lock Record空间不需要初始化(后面会用到它),然后直接用CAS将自己的线程ID写到对象的MarkWord里,如果CAS操作成功,就获取了偏向锁。线程获取偏向锁后即便是执行完加锁的代码块,也会一直持有锁不会主动释放。因此这个线程以后每次进入这个锁相关的代码块的时候,都不需要执行任何额外的同步操作。

当有另外一个线程尝试获得锁的时候,需要进行revoke操作,分情况讨论:

判断持有偏向锁的线程是否还活着,如果线程不处于活动状态,则偏向锁被重置为无锁状态。

如果持有偏向锁的线程还活着而且当前线程实际没有持有着锁,则偏向锁被重置为无锁状态。

如果持有偏向锁的线程还活着而且当前线程实际持有着锁(在同步代码块中),那么试图获得偏向锁的线程将等待一个全局安全点(global safepoint),在全局安全点,【试图获得偏向锁的线程】操作【持有偏向锁的线程的线程栈】,遍历里面的所有栈帧里的所有与当前锁对象相关联的LockRecord,修改LockRecord里的内容为轻量级锁的LockRecord应该有的内容,然后把“最老的”(oldest)一个LockRecord的指针写到对象的MarkWord里,至此,就好像是原来从没有使用过偏向锁,使用的一直是轻量级锁。

上面的第3点基本是照着官方文档翻译的,看了一些书、博客,对这块都说的不明白。

以下是我自己的理解:

一个已经持有偏向锁的线程,再次进入这个锁相关的代码块的时候,虽然不需要执行额外的同步操作,但是依旧会在栈上生成一个空的LockRecord,因此对于一个重入了几次对象锁的线程来说,栈中就有了关联同一个对象的多个LockRecord。

而且在对象的MarkWord里,会记录着加锁的次数,每重入一次,就+1;当每次要解锁的时候,首先会把对象MarkWord里的加锁次数-1,只有当加锁次数减到0的时候,才真正的去执行加锁操作。这个是参考了monitorexit字节码的解释来的:

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/bdd7864195536f77ad276fc726b9bc62.html