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 在一个工作线程中,都会获取到锁。
欢迎关注订阅微信公众号,程序员的压哨绝杀,一起分享生活工作中的见闻趣事。