Java中的几把JVM锁

synchronized

synchronized是一把非常经典的锁,在招聘的时候也是常问的高频问点。同时也是平时我们在做开发并发程序中用的比较多的锁。在JDK1.6之前synchronized是一把重量级锁,不过在不过的优化后,如今他已变得不那么重了,甚至在某些场景中,它的性能反而更优于轻量级锁。

synchronized锁的特点:

        在jdk1.6之前synchronized是一把重量级锁,不过在jdk1.6后大佬们对synchronized锁进行了一系列优化使得他变得不那么重,这就是典型的锁升级的过程,那么我们就来具体聊聊。因为synchronized是对象锁,所以不得不先要了解一下Java对象的内存结构。

如图所示,在创建一个对象后,在JVM虚拟机(HotSpot)中,对象在Java内存中的存储布局可分为三块:

        对象头区域

               1. 对象自身的运行时数据区(MarkDown)

              一般用来存储对象的hashcode、GC分代年龄、锁类型标记、偏向锁线程ID、CAS锁指向线程LockRecord的指针等,synchronized锁的机制与这个部分(markdown)密切相关,用markdown中最低的三位代表锁的状态,其中一位时偏向锁位,另外两位时普通锁位。

               2. 实例数据区

                此处存储的是对象真正有效的信息,比如对象中所有字段的内容

               3. 对齐填充区域

                JVM 的实现 HostSpot 规定对象的起始地址必须是 8 字节的整数倍,换句话来说,现在 64 位的 OS 往外读取数据的时候一次性读取 64bit 整数倍的数据,也就是 8 个字节,所以 HotSpot 为了高效读取对象,就做了"对齐",如果一个对象实际占的内存大小不是 8byte 的整数倍时,就"补位"到 8byte 的整数倍。所以对齐填充区域的大小不是固定的。

当线程进入到 synchronized 处尝试获取该锁时, synchronized 锁升级流程如下:

如上图所示, synchronized 锁升级的顺序为:偏向锁->轻量级锁->重量级锁,每一步触发锁升级的情况如下:

偏向锁:

        在JDK1.8中,其实默认的是轻量级锁,但是如果设定了 -XX:BiasedLockingStartupDelay = 0 ,那么对一个Object做synchronized的时候,会立即上一把偏向锁。当处于偏向锁状态时,markdown会记录当前的线程ID。

升级到轻量级锁:

        当下一个线程参与到偏向锁竞争的时候,会线判断参与竞争的线程ID与偏向锁中记录的线程ID是否一致,如果不一致则会撤销偏向锁,升级为轻量级锁。每个线程在自己的线程栈中生成一个LockRecord(LR),然后每个线程通过CAS(自旋)的操作将锁对象头中的markdown设置为指向自己的LR的指针,哪个线程设置成功,就意味着获得了锁。

升级到重量级锁:

        如果锁竞争加剧(如线程自旋次数或者自旋的线程数超过一定的阈值),就会升级为重量级锁。此时就会向操作系统申请资源,线程挂起,进入到操作系统内核态的等待队列中,等待操作系统调度,然后映射回用户态。在重量级锁中,由于需要做内核态到用户态的转换,而这个过程中需要消耗较多时间,也就是"重"的原因之一。

可重入:

        synchronized 拥有强制原子性的内部锁机制,是一把可重入锁。因此,在一个线程使用 synchronized 方法时调用该对象另一个 synchronized 方法,即一个线程得到一个对象锁后再次请求该对象锁,是永远可以拿到锁的。在 Java 中线程获得对象锁的操作是以线程为单位的,而不是以调用为单位的。 synchronized 锁的对象头的 markwork 中会记录该锁的线程持有者和计数器,当一个线程请求成功后, JVM 会记下持有锁的线程,并将计数器计为1。此时其他线程请求该锁,则必须等待。而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增。当线程退出一个  synchronized 方法/块时,计数器会递减,如果计数器为 0 则释放该锁锁。

悲观锁(互斥锁、排他锁)

        synchronized 是一把悲观锁(独占锁),当前线程如果获取到锁,会导致其它所有需要锁该的线程等待,一直等待持有锁的线程释放锁才继续进行锁的争抢。

ReentrantLock

ReentrantLock从字面上来看,也是一把可重入锁,这一点和synchronized一样,但是实现原理和synchronized有很大区别,它是基于经典的AQS实现的,AQS时基于voliate和CAS实现的,其中AQS中维护了一个voliate类型的变量state来做一个可重入的次数,加锁和释放锁也是围绕这个变量来进行的。同时ReentrantLock也提供了一些synchronized没有的特点。

AQS模型如下图:

ReentrantLock有如下特点:

1.可重入

         ReentrantLock 和 syncronized 关键字一样,都是可重入锁,不过两者实现原理稍有差别, RetrantLock 利用 AQS 的的 state 状态来判断资源是否已锁,同一线程重入加锁, state 的状态 +1 ; 同一线程重入解锁, state 状态 -1 (解锁必须为当前独占线程,否则异常); 当 state 为 0 时解锁成功。

2.需要手动加锁、解锁

        synchronized 关键字是自动进行加锁、解锁的,而 ReentrantLock 需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成,来手动加锁、解锁。

3、支持设置锁的超时时间

        synchronized 关键字无法设置锁的超时时间,如果一个获得锁的线程内部发生死锁,那么其他线程就会一直进入阻塞状态,而 ReentrantLock 提供 tryLock 方法,允许设置线程获取锁的超时时间,如果超时,则跳过,不进行任何操作,避免死锁的发生。

4、支持公平/非公平锁

        synchronized 关键字是一种非公平锁,先抢到锁的线程先执行。而 ReentrantLock 的构造方法中允许设置 true/false 来实现公平、非公平锁,如果设置为 true ,则线程获取锁要遵循"先来后到"的规则,每次都会构造一个线程 Node ,然后到双向链表的"尾巴"后面排队,等待前面的 Node 释放锁资源。

5、可中断锁

        ReentrantLock 中的 lockInterruptibly() 方法使得线程可以在被阻塞时响应中断,比如一个线程 t1 通过 lockInterruptibly() 方法获取到一个可重入锁,并执行一个长时间的任务,另一个线程通过 interrupt() 方法就可以立刻打断 t1 线程的执行,来获取t1持有的那个可重入锁。而通过 ReentrantLock 的 lock() 方法或者 Synchronized 持有锁的线程是不会响应其他线程的 interrupt() 方法的,直到该方法主动释放锁之后才会响应 interrupt() 方法。

ReentrantReadWriteLock

       ReentrantReadWriteLock 顾名思义实际上就是读写锁,同时其实他是两把锁,一把是写锁,一把是读锁。读写锁的规则是:读读不互斥、读写互斥、写写互斥。在一些实际的场景中,读操作的频率远远高于写操作,如果直接用一般的锁进行并发控制的话,就会读读互斥、读写互斥、写写互斥,效率低下。所以读写锁就应运而生了。

        ReentrantReadWriteLock的原理也是基于AQS进行实现的,与 ReentrantLock 的差别在于 ReentrantReadWriteLock 锁拥有共享锁、排他锁属性。读写锁中的加锁、释放锁也是基于Sync(继承AQS),并且主要使用AQS中的state和node中的waitState变量进行实现的。实现读写锁与实现普通互斥锁的主要区别在于需要分别记录读锁状态及写锁状态,并且等待队列中需要区别处理两种加锁操作。 ReentrantReadWriteLock 中将 AQS 中的 int 类型的 state 分为高 16 位与第 16 位分别记录读锁和写锁的状态,如下图所示:

参考:

       聊聊 Java 的几把 JVM 级锁 (qq.com) 

  • 29
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值