synchronized是托管给JVM执行的,Lock的锁定是通过代码实现的。所以Lock比较灵活,可以便于开发人员根据合适的场景进行操作,Lock是一个接口,需要实现它来进行使用,ReetrantLock是Lock的主要实现类,ReetrantLock是一个可重入锁,同时可以指定公平锁和非公平锁,我们来具体看一下他的实现方式。
一、ReentrantLock使用方式
ReentrantLock lock = new ReentrantLock();
//如果被其它线程占用锁,会阻塞在此等待锁释放
lock.lock();
try {
//操作
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放锁
lock.unlock();
}
上面只是其中一种方式,可以看到ReentrantLock的使用方式比较简单,创建出一个ReentrantLock对象,通过lock()方法进行加锁,使用unlock()方法进行释放锁操作。
他的加锁方式有三种,使用lock、trylock、trylock(long,TimeUnit)指定时间参数。使用lock来获取锁的话,如果锁被其他线程持有,那么就会处于等待状态。另外需要我们去主动的调用unlock方法去释放锁,即使发生异常,他也不会主动释放锁,需要我们显式的释放。使用trylock方法获取锁,是有返回值的,获取成功返回true,获取失败返回false,不会一直处于等待状态。使用trylock(long,TimeUnit)指定时间参数来获取锁,在等待时间内获取到锁返回true,超时返回false。还可以调用lockInterruptibly方法去中断锁,如果线程正在等待获取锁,可以中断线程的等待状态。
二、什么是可重入锁
上面我们说ReentrantLock是一个可重入锁,那么,什么是可重入锁呢?可重入锁是指,线程可对同一把锁进行重复加锁,而不会被阻塞住,这样可避免死锁的产生。我们先来看下验证可重入锁的代码:
public static class TestReentrantLock {
private Lock lock = new ReentrantLock();
public void method() {
lock.lock();
try {
System.out.println("方法1获得ReentrantLock锁");
method2();
} finally {
lock.unlock();
}
}
public void method2() {
lock.lock();
try {
System.out.println("方法2重入ReentrantLock锁");
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
new TestReentrantLock().method();
}
}
由上面的代码我们可以得知,ReentrantLock是具有可重入性的。那么,这种可重入的底层实现方式是什么呢?
可重入锁的底层实现方式
ReentrantLock是使用AQS中的state的值,线程可以不停地lock来增加state的值,对应地需要unlock来解锁,直到state为零。并且state的值是用volatile进行修饰,以下是具体源码实现。
//java.util.concurrent.locks.ReentrantLock.FairSync
protected final boolean tryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
int c = getState();
//当前锁没被占用
if (c == 0) {
//1.判断同步队列中是否有节点在等待
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {//2.如果上面!1成立,修改state值(表明当前锁已被占用)
//3.如果2成立,修改当前占用锁的线程为当前线程
setExclusiveOwnerThread(current);
return true;
}
}
//占用锁线程==当前线程(重入)
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;//
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
//修改status
setState(nextc);
return true;
}
//直接获取锁失败
return false;
}
三、什么是公平锁
我们前面说了ReentrantLock可以实现公平锁和非公平锁,那么什么是公平锁?什么是非公平锁呢?
所谓的公平锁,就是在多个线程请求获取同一个资源的时候,能够保证依照线程请求的顺序,依次执行,来保证公平竞争的效果。反之,非公平锁就是不按照线程请求顺序,每个线程一起去争抢锁,谁抢到是谁的。
上面我们说到ReentrantLock中的公平锁是通过继承AQS来实现的,我们来看下他的具体实现方式。
AQS
AQS是一个抽象类,主要是通过继承的方式来使用。AQS的功能分为两种:独占和共享。AQS的实现依赖内部的同步队列,也就是FIFO的双向队列,如果当前线程竞争锁失败,那么AQS会把当前线程以及等待状态信息构造成一个Node加入到同步队列中,同时再阻塞该线程。当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)。AQS使用一个int类型的成员变量state来表示同步状态,当state>0时表示已经获取了锁,当state = 0时表示释放了锁。
当有锁竞争的时候,当有新的线程加入进来,会将此线程封装成Node节点追加到同步队列中,并将新线程的前置指针指向上一个节点,将上一个节点的后置指针指向新线程的节点。是通过CAS来进行指针指向的这个修改的。
当头结点在释放锁时,会唤醒后继节点,如果后继节点获得锁成功,会把自己设置为头结点,设置头节点不需要用CAS,原因是设置头节点是由获得锁的线程来完成的,而同步锁只能由一个线程获得,所以不需要CAS保证。
公平锁源码
加入同步队列(当同步队列为空时会直接获得锁),等待锁
//java.util.concurrent.locks.ReentrantLock.FairSync
final void lock() {
acquire(1);
}
//java.util.concurrent.locks.AbstractQueuedSynchronizer
public final void acquire(int arg) {
if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire():模板方法,获取锁
//java.util.concurrent.locks.AbstractQueuedSynchronizer
//1
private Node addWaiter(Node mode) {
//生成node
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
//将node加到队列尾部
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//如果加入失败(多线程竞争或者tail指针为null)
enq(node);
return node;
}
//1.1
private Node enq(final Node node) {
//死循环加入节点(cas会失败)
for (;;) {
Node t = tail;
if (t == null) { //tail为null,同步队列初始化
//设置head指针
if (compareAndSetHead(new Node()))//注意这里是个空节点!!
tail = head;//将tail也指向head
} else {
node.prev = t;//将当前node加到队尾
if (compareAndSetTail(t, node)) {
t.next = node;
return t;//注意这里才返回
}
}
}
}
//2
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
//表示是否被打断
boolean interrupted = false;
for (;;) {
//获取node.pre节点
final Node p = node.predecessor();
if (p == head //当前节点是否是同步队列中的第二个节点
&& tryAcquire(arg)) {//获取锁,head指向当前节点
setHead(node);//head=head.next
p.next = null;//置空
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) && //是否空转(因为空转唤醒是个耗时操作,进入空转前判断pre节点状态.如果pre节点即将释放锁,则不进入空转)
parkAndCheckInterrupt())//利用unsafe.park()进行空转(阻塞)
interrupted = true;//如果Thread.interrupt()被调用,(不会真的被打断,会继续循环空转直到获取到锁)
}
} finally {
if (failed)//tryAcquire()过程出现异常导致获取锁失败,则移除当前节点
cancelAcquire(node);
}
}
acquireQueued(addWaiter(Node.EXCLUSIVE), arg):加入同步队列
//java.util.concurrent.locks.AbstractQueuedSynchronizer
//1
private Node addWaiter(Node mode) {
//生成node
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
//将node加到队列尾部
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//如果加入失败(多线程竞争或者tail指针为null)
enq(node);
return node;
}
//1.1
private Node enq(final Node node) {
//死循环加入节点(cas会失败)
for (;;) {
Node t = tail;
if (t == null) { //tail为null,同步队列初始化
//设置head指针
if (compareAndSetHead(new Node()))//注意这里是个空节点!!
tail = head;//将tail也指向head
} else {
node.prev = t;//将当前node加到队尾
if (compareAndSetTail(t, node)) {
t.next = node;
return t;//注意这里才返回
}
}
}
}
//2
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
//表示是否被打断
boolean interrupted = false;
for (;;) {
//获取node.pre节点
final Node p = node.predecessor();
if (p == head //当前节点是否是同步队列中的第二个节点
&& tryAcquire(arg)) {//获取锁,head指向当前节点
setHead(node);//head=head.next
p.next = null;//置空
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) && //是否空转(因为空转唤醒是个耗时操作,进入空转前判断pre节点状态.如果pre节点即将释放锁,则不进入空转)
parkAndCheckInterrupt())//利用unsafe.park()进行空转(阻塞)
interrupted = true;//如果Thread.interrupt()被调用,(不会真的被打断,会继续循环空转直到获取到锁)
}
} finally {
if (failed)//tryAcquire()过程出现异常导致获取锁失败,则移除当前节点
cancelAcquire(node);
}
}
selfInterrupt(): 唤醒当前线程
static void selfInterrupt() {//在获取锁之后 响应intterpt()请求
Thread.currentThread().interrupt();
}
以上就是ReadWriteLock原理及源码的解读,希望大家能有收获。
部分内容参考:https://juejin.im/post/5ae1b4f0f265da0b7b359d7a