你真的明白ReentrantLock了吗?

·  阅读 1815

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原理及源码的解读,希望大家能有收获。

部分内容参考:juejin.cn/post/684490…

分类:
阅读
标签:
分类:
阅读
标签:
收藏成功!
已添加到「」, 点击更改