ReentrantLock 源码简单分析

JAVA中锁的实现最常见的方式有两种,一种是 synchronized关键字,一种是Lock。实际的开发过程中,要对这两种方式进行取舍。 

synchronized是基于JVM层面实现的, Lock却是基于JDK实现的。

synchronized是一个关键字,使用简单,锁粒度粗。Lock相对复杂,需要释放,锁粒度自由。Lock 功能相对强大,如下表。

tips

synchronized

Lock

锁获取超时

不支持

支持

获取锁响应中断

不支持

支持

下面开始正文,ReentrantLock  是Lock的一种实现,ReentrantLock 是可重入的互斥锁,互斥很好理解,可重入锁单独解释一下:可重复可递归调用的锁,意味着线程可以进入任何一个它已经拥有的锁所同步着的代码块。

ReentrantLock 有三种加锁方法,分别为lock()、tryLock()、和lockInterruptibly()。下面先简单的介绍一下这三个方法的区别。

1)lock(), 拿不到lock就不罢休,不然线程就一直block。

2)tryLock(),马上返回,拿到lock就返回true,不然返回false。

带时间限制的tryLock(),拿不到lock,就等一段时间,超时返回false。

3)lockInterruptibly()

1. 线程在sleep或wait,join, 此时如果别的进程调用此进程的 interrupt()方法,此线程会被唤醒并被要求处理InterruptedException;

2. 此线程在运行中, 则不会收到提醒。但是 此线程的 “打扰标志”会被设置,可以通过isInterrupted()查看并作出处理。

那么下面我们就来看一下ReentrantLock 的源码。

首先我们来看下lock()方法.

sync 可以有两个NonfairSync和FairSync实现,调用构造函数的时候可以选择,区别是:

如果当前线程不是锁的占有者,则NonfairSync并不判断是否有等待队列,直接使用compareAndSwap去进行锁的占用,这样会减少线程挂起和唤醒的开销。

如果当前线程不是锁的占有者,则FairSync则会判断当前是否有等待队列,如果有则将自己加到等待队列尾,保证锁占用的公平性。

下面继续看lock源码。

NonfairSync和FairSync 的区别就是 compareAndSetState 这方法,那么,我们来看下这个方法里面是干什么的。

 

Unsafe类是在sun.misc包下,不属于Java标准,但是很多被广泛使用的高性能开发库都是基于Unsafe类开发的。

compareAndSwapInt 方法 尝试使用乐观锁技术,如果当前状态值等于期望值,则自动将同步状态设置为给定的更新值。 

这就解释了NonfairSync 是 偏向锁,新进来的线程不需要进入队列,而是可以直接尝试获取锁,这也节省了线程的开销,而FairSync 获取锁需要执行acquire(1) 方法,这个方法后面讲。

获取线程成功之后,调用setExclusiveOwnerThread方法,记录当前线程。

记录这个线程有什么用呢,上面说了,ReentrantLock 是可重入锁,记录线程,实现可重入功能,下面继续看。

如果使用compareAndSetState获取不到锁,执行到了acquire(1) 这个方法。

 

以NonfairSync 为例,我们再来看下tryAcquire(1)这个方法的实现。

由于ReentrantLock 是可重入锁,state是当前锁的占有数量 ,getState()获取state,如果没人占用,使用CAS的方式进行占用,不直接占用是防止冲突,如果没有占用上或者线程数不等于0,则走到else里,判断是否满足可重入机制,满足的话,state增加,更新state。

FairSync 的tryAcquire() 方法也类似。

由于FairSync 是公平锁,所以需要判断线程是否在队列的投,如果不在,则调用CAS加锁的方法。后面可重入机制与NonfairSync一样。

如果tryAcquire()没有获取到锁,则需要继续往下走addWaiter()和acquireQueued() 方法。

addWaiter() 和enq() 就是将节点插入排队队列,为空时创建,没什么好说的

acquireQueued() 方法,一个for(;;)的死循环,如果不是头结点,会停止循环,否则直到线程成功获取到锁。所以从这可以看出,lock()方法 是没有中断机制的。

shouldParkAfterFailedAcquire() 方法检查更新未能获取数据的节点的状态。其中compareAndSetWaitStatus() 用CAS的方法给线程SIGNAL标识,保证持续有线程获取锁。

parkAndCheckInterrupt()检查是否中断。

这里对waitStatus 的一些状态有疑惑,我们来看下Node的属性,都是因为,没关系,有道词典翻译一下。

CANCELLED: 表示线程已被取消的waitStatus值,即这个node失效了,可能中断、超时等原因。

SIGNAL: 后续线程需要阻塞,所以当前node在释放锁时必须启动后续线程,所以这个代表线程启动的信号。

CONDITION: 指示线程正处于等待状态,这个node当前是属于条件锁。

PROPAGATE: 无条件传播,这个node是共享锁节点,他需要进行唤醒传播。

 

cancelAcquire() 方法是取消正在进行的获取尝试。

selfInterrupt()就是将对节点进行一些列的设置,取消的丢弃,设置次线程状态为取消,头结点唤醒等操作。 核心代码是node.waitStatus = Node.CANCELLED;将线程的状态改为CANCELLED。

 

根据上面的源码,我们来分析一下acquire()方法的,就是请求独占锁,忽略所有中断,首先tryAcquire,如果成功就返回,否则线程进入队列,循环切换阻塞&唤醒状态,直到tryAcquire成功。

 

 

tryLock()方法就是lock() 方法的简版,只使用nonfairTryAcquire()方法获取一次锁,无论成功失败都返回,但依旧保留了可重入机制。

lockInterruptibly() 方法和 lock() 类似。就是可以中断的lock(),acquireInterruptibly()代替acquire()方法,acquireInterruptibly() 方法的与acquire()的区别就在doAcquireInterruptibly() 这个方法中增加了中断功能,而acquireInterruptibly() 也捕获了InterruptedException异常。

 

parkAndCheckInterrupt()方法会检查是否有中断标识,有的话并且线程是阻塞的,会抛出InterruptedException()异常。

 

加锁的方法分析完了,简单的分析一下解锁的方法 unlock(),照理,源码上图。

 

tryRelease()方法会对state进行减一操作,然后判断当前线程是否和持有锁线程一致,如果不一致,抛出异常,继续判断state的值,只有当值为0时,free标志才置为true,否则说明是重入锁,同线程继续占用,需要多次释放直到state为0。

下面看一个可重入锁的小例子。从使用方法中理解一下。

先看lock()的例子

 

分别执行test1() 和 test2() ,结果应该是一样的,因为lock()不获取到锁,是不会结束的。

 

而如果把lock()换成trylock()结果就完全不同,看例子。

还是分别执行test1() 和test2()两个方法

 

用可重入锁的机制,很简单的就分析出来了,test1 不在一个工作线程中,会有2个线程没有获取到锁。test2 在一个工作线程中,都会获取到锁。

 

欢迎关注订阅微信公众号,程序员的压哨绝杀,一起分享生活工作中的见闻趣事。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值