目录
6.ReentrantLock和Synchronized的共同点和不同点
ReentrantLock作为Java中除了synchronized之外用的最多的锁。ReentrantLock是利用AQS框架实现的乐观锁,在介绍ReentrantLock之前先看一下AQS也就是AbstractQueuedSynchronizer。
1.AQS
AQS是一个同步框架用来控制多线程访问共享资源。使得多线程有顺序的去访问共享资源。AQS中有两个很重要的元素
A.volatitle修饰状态值state,线程通过CAS操作来改变state的值来作为这个线程是否获取到锁的依据。一般都是默认state等于0的时候,这个时候锁是没有被其他线程获取的,通过CAS操作改变了state的值,如果改变成功说明获取到了锁,如果改变不成功,说明没有获取到锁
B.同步FIFO队列,当线程没有成功获取锁的时候,会将这个线程加入到队列中,然后当线程释放锁后,会从队列中唤醒一个线程重新占有锁。这个队列是一个双向队列,但这个队列不是一个真实声明的队列,它是通过一个个的Node组成的,每个Node都是对没有获取到锁的线程的封装,而且Node是有指向它前面和后面Node的指针,这样多个Node组合在一起就形成了一个双向队列,这个Node是AbstractQueuedSynchronizer里面的内部类。队列的结构图
Node对象的代码
static final class Node {
//标记表示节点正在共享模式中等待
static final Node SHARED = new Node();
//标记表示节点正在独占模式下等待
static final Node EXCLUSIVE = null;
//waitStatus值表示线程已取消
static final int CANCELLED = 1;
//waitStatus值表示后继者的线程需要被唤醒
static final int SIGNAL = -1;
//waitStatus值表示线程正在等待条件
static final int CONDITION = -2;
//waitStatus值表示下一个acquireShared应无条件传播
static final int PROPAGATE = -3;
volatile int waitStatus;
// 当前节点的前驱节点
volatile Node prev;
// 当前节点的后续节点
volatile Node next;
// 当前节点指向的线程
volatile Thread thread;
// nextWaiter是“区别当前CLH队列是 ‘独占锁’队列 还是 ‘共享锁’队列 的标记”
// 若nextWaiter=SHARED,则CLH队列是“共享锁”队列;
// 若nextWaiter=EXCLUSIVE,(即nextWaiter=null),则CLH队列是“独占锁”队列。
Node nextWaiter;
/**
* Returns true if node is waiting in shared mode.
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
/**
* Returns previous node, or throws NullPointerException if null.
* Use when predecessor cannot be null. The null check could
* be elided, but is present to help the VM.
*
* @return the predecessor of this node
* 返回前驱节点
*/
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
// Used by addWaiter
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
// Used by Condition
Node(Thread thread, int waitStatus) {
this.waitStatus = waitStatus;
this.thread = thread;
}
}
用一句话总结AQS框架就是线程通过CAS操作来改变state的值,如果成功说明这个线程拿到锁了,如果不成功就放入队列中挂起,等待它的前一个线程来唤醒它重新对state进行CAS操作来获取锁
2.ReentrantLock经典题
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author wangbiao
* @Date 2019-11-17 16:59
* @Decripition TODO
**/
public class ReetrantLockTest {
private Lock lock = new ReentrantLock();
private Condition conditionA = lock.newCondition();
private Condition conditionB = lock.newCondition();
private Condition conditionC = lock.newCondition();
public void printA(){
try{
lock.lock();
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"打印A");
conditionB.signal();
conditionA.await();
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void printB(){
try{
try{
lock.lock();
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"打印B");
conditionC.signal();
conditionB.await();
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}catch (Exception e){
e.printStackTrace();
}
}
public void printC(){
try{
try{
lock.lock();
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"打印C");
conditionA.signal();
conditionC.await();
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}catch (Exception e){
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception{
ReetrantLockTest test = new ReetrantLockTest();
Thread threadA = new Thread(){
public void run(){
test.printA();
}
};
Thread threadB = new Thread(){
public void run(){
test.printB();
}
};
Thread threadC = new Thread(){
public void run(){
test.printC();
}
};
threadA.start();
Thread.sleep(100);
threadB.start();
Thread.sleep(100);
threadC.start();
}
}
最后输出:
Thread-0打印A
Thread-1打印B
Thread-2打印C
Thread-0打印A
Thread-1打印B
Thread-2打印C
Thread-0打印A
Thread-1打印B
Thread-2打印C
Thread-0打印A
Thread-1打印B
Thread-2打印C
Thread-0打印A
Thread-1打印B
Thread-2打印C
Thread-0打印A
Thread-1打印B
Thread-2打印C
Thread-0打印A
Thread-1打印B
Thread-2打印C
Thread-0打印A
Thread-1打印B
Thread-2打印C
Thread-0打印A
Thread-1打印B
Thread-2打印C
Thread-0打印A
Thread-1打印B
Thread-2打印C
3.ReentrantLock源码分析
3.1ReentrantLock结构图
ReentrantLock实现了Lock接口,并且内部有三个内部类Sync,NonFairSyn和FairSync,这里运用了模板模式,在AbstractQueuedSynchronizer里面实现了加锁和释放锁的抽象操作,就是大体的操作流程,但是具体实现例如tryAcquire,tryRelease这些方法就要靠具体的实现类来实现了,我觉得还用到了适配器模式,通过实现Lock接口,但在实现方法里面引用了Sync的具体方法。
3.2ReentrantLock的构造方法
可以发现ReentrantLock默认的是非公平锁,如果在申明ReentrantLock的时候,向构造器里面传入一个true,那ReentrantLock就是一个公平锁
public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
// 同步器的引用
private final Sync sync;
//非公平锁的构造函数
public ReentrantLock() {
sync = new NonfairSync();
}
// 公平锁的构造函数
public ReentrantLock(boolean fair) {
// 三目运算符,如果为true 则为公平锁,反之为非公平锁
sync = fair ? new FairSync() : new NonfairSync();
}
}
3.3获取锁lock()方法
public void lock() {
sync.lock();
}
可以看到lock方法实际调用的是NonFairSync里面的lock方法,先直接利用CAS操作尝试去改变state的值,如果成功,就说明获取到了锁,如果没有成功再调用acquire,公平锁的lock方法和这里不太一样,公平锁的lock不会直接去尝试改变state的值,而是直接调用acquire方法。
NonFairSync.lock方法
final void lock() {
//尝试直接改变state的值,如果成功说明获取到锁
if (compareAndSetState(0, 1))
//将锁的独占线程设置为当前线程
setExclusiveOwnerThread(Thread.currentThread());
else
//如果直接尝试改值失败,就进行下面的操作
acquire(1);
}
AbstractQueuedSynchronizer.acuqre方法
/**
* tryAcquire(arg)方法
* 1.尝试通过CAS操作改变state值来拿到锁
* addWaiter(AbstractQueuedSynchronizer.Node.EXCLUSIVE)
* 2.操作失败就将线程封装成Node并放入队尾
* acquireQueued(addWaiter(AbstractQueuedSynchronizer.Node.EXCLUSIVE), arg)
* 3。放入到队尾后判断上一个节点是否是队头,如果是的,就尝试获取锁,获取失败就挂起
* 如果不是队头就挂起
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(AbstractQueuedSynchronizer.Node.EXCLUSIVE), arg))
//如果没有获取到锁,但是有中断操作,就自己中断
selfInterrupt();
}
到回到NonFairSync的tryAcquire方法,先判断state是否等于0,等于0说明当前锁没有被占用,利用CAS操作更改state的值,要是更改成功说明获取锁成功,如果state不等于0,就判断占有锁的线程是不是当前线程,要是是的,就对state进行加一操作。
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
/**
* 先尝试利用CAS操作更改state的值,如果更改成功,说明拿到锁了,
* 如果不成功,说明这个锁已经有线程占有了,那就判断占有这个锁的线程是否是当前线程
* 如果是的,state值进行加一操作,这里也体现了ReentrantLock是可重入锁
* @param acquires
* @return
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//获取当前锁的状态值,如果是等于0,说明没有线程占有这个锁
int c = getState();
if (c == 0) {
//通过cas改变状态值,如果更改成功,说明取得了锁
if (compareAndSetState(0, acquires)) {
//将拥有锁的线程替换成当前线程
setExclusiveOwnerThread(current);
return true;
}
}
//如果拥有锁的线程是当前线程,则将状态值加一
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
AbstractQueuedSynchronized的addWaiter方法,这个方法就是将当前线程封装成Node,并快速添加到队尾,如果快速添加失败,就通过for的无限循环插入到队尾
private Node addWaiter(Node mode) {
//先将当前线程封装成Node,由于mode是null,所以是独占模式
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
// 获取队尾,通过cas快速替换当前tail节点为node节点
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//如果快速插入失败,则利用无限for循环进行插入操作
enq(node);
return node;
}
/**
* 一种常见的CAS操作,就是无限for循环,直到CAS操作成功才跳出for循环
* @param node
* @return
*/
private Node enq(final Node node) {
for (;;) {
Node t = tail;
//如果队尾是空的,就先初始化一个节点,并设置为头节点,队尾也指向头节点
//然后执行完后再循环一次,队尾就不会为空了
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
//将node设置为队尾
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
成功将Node插入到队尾后,判断这个队尾的前一个Node是不是队头,如果是队头的话尝试获取锁,如果它前面的节点不是队头的话就挂起,等待被唤醒后再次执行判断它前面的Node是不是队头,如果是队头就尝试获取锁,如果获取成功,就将这个节点设置成头节点。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 死循环,直到满足条件退出
// 首先将当前线程进行阻塞,等待其他线程进行唤醒
// 其他线程进行唤醒以后,判断当前是否获取资源。此时,可能有其他线程的加入,导致获取失败
for (;;) {
// 获取当前节点的前驱节点
final Node p = node.predecessor();
// 如果当前节点的前驱节点是head节点,且尝试获取锁成功
if (p == head && tryAcquire(arg)) {
// 设置当前的node节点为head节点
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 判断当前线程是否应该阻塞
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 如果当前线程被中断,则 parkAndCheckInterrupt()返回为true。interrupted = true
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
/**
* 如果Node的前节点的waitStatus是等于1的,那么就直接可以确认将Node挂起的
* 如果waitStatus是大于0的,则代表线程被取消,重新设置Node的前驱节点
* 找到第一个ws<=0的节点,将这个节点设置为Node的前驱节点
* 默认的Node的ws是null的,所以会将ws设置为SIGNAL,也就是-1
*
* @param pred
* @param node
* @return
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
将当前线程挂起,挂起后如果被唤醒,就判断当前线程是否被中断了
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
总结一下ReentrantLock非公平锁的获取:
1.尝试通过cas操作来改变state的值,如果能改变则获取到锁。
2.如果没能改变state的值,则判断state的值是否为0,如果为0则尝试用cas操作来改变state值来拿锁。
3.如果state的值不为0,则判断拿到锁的线程是否是自己,如果是的,则state加一操作,如果获取锁的线程不是自己,则将自己封装成一个node节点,放入到队尾。
4.放入队尾后,判断自己是否是头节点,如果是头节点,则尝试改变state的值获取锁,如果不是头节点,则挂起来,等待前面的节点来唤醒。
3.4 释放锁
调用的AbstractQueuedSynchronizer的release方法,
public void unlock() {
sync.release(1);
}
/**
* 如果释放资源成功,并且头节点不为空,并且头节点的状态值不等于0,
* 就唤醒下一个Node节点来获取锁
* @param arg
* @return
*/
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
tryRelease方法,锁的状态值进行减一操作,如果状态值等于0,就说明这个锁被完全释放
/**
* 先对锁的状态值进行减一操作,如果当前线程不是获取到锁的线程,就抛出IllegaMonitorStateException异常
* 如果状态值在减一后变成0,那么说明这个锁被完全释放了,将占有锁的线程设置成空
* @param releases
* @return
*/
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
释放完锁后就需要唤醒下一个节点来获取锁,AbstractQueuedSynchronizer的unparkSuccessor
private void unparkSuccessor(AbstractQueuedSynchronizer.Node node) {
//把状态值设为0
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/**
* 获取Node的后驱元素,如果后驱节点是空,或者状态是取消,
* 要找到离队头最近的状态是小于等于0的节点,然后就将这个节点唤醒
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
4.总结ReentrantLock的基本流程:
这里是对于非公平锁而言的流程
加锁的过程
A.直接利用CAS更改同步状态值,如果成功,就将锁的独占线程设置成当前线程,说明获取到锁
B.更改状态值失败,在tryAcquire方法里面获取状态值,如果状态值等于0,就利用CAS操作更改状态值,成功说明获取到锁
如果状态值不等于0,判断拥有锁的线程是不是当前线程,如果是的,就对状态值加一操作,拿到锁
C.B过程中没有拿到锁,就将当前线程封装成Node节点,并快速插入到队尾,如果快速插入失败的话,要是队头为null的话,先初始化一个Node节点作为队尾,再然后将Node节点作为队尾插入。
D.插入队尾完成后,利用无限for循环,先判断当前Node节点的前驱节点是否是队头,如果是的,尝试获取锁,如果获取成功就直接返回,如果失败或者前驱节点不是队头,判断自己的前驱节点的状态是否是SIGNAL,SIGNAL代表可以唤醒后驱节点,如果是的直接返回true,如果不是的,就找到最近的一个状态是SIGNAL的节点作为前驱节点返回true,再将自己挂起,等待唤醒。
E.挂起后,如果被唤醒,判断自己是否被中断了,如果被中断就自己尝试中断自己。
解锁的过程
A.更改状态值也就是减一操作,判断当前线程是否是持有锁的线程,如果是的,则判断释放后的状态值是否为0,如果是的说明锁被完全释放,返回true,如果锁没有被完全释放,返回false
B.如果锁被完全释放,就唤醒队头的后驱节点,唤醒的前提是后驱节点的waitstatus是小于等于0的,如果不是的,找到离队头最近的waitstatus是小于等于0的节点进行唤醒,唤醒调用LockSupport.unpark(s.thread);
5.ReentrantLock公平锁
ReentrantLock默认的是非公平锁,当申明ReentrantLock实例的时候向构造函数里面传参数true的时候,就是公平锁了,非公平锁和公平锁最多的区别是在都调用lock方法的时候:
非公平锁调用lock方法
首先是直接去更改锁的状态值来尝试获取锁,如果获取失败了,再进行下面的操作
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
公平锁加锁的操作:
final void lock() {
acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
在FairSync重写tryAcquire方法的时候,去获取锁的前面加了一个hasQueuedPredecessors的判断,这个判断是判断队列中有没有比自己排在更前面的节点,也就是头节点的下一个节点是否存在。
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
总结一下公平锁和非公平锁在获取锁的过程中有什么不同:
非公平锁加锁的时候是直接先去利用CAS操作更改锁的状态值来判断有没有拿到锁,如果没有拿到,再去判断锁的状态值是否等于0,如果等于0再去尝试获取锁。
公平锁加锁的时候会先去获取锁的状态值是否等于0,如果等于0,还要去判断当前队列中是否有排在自己前面的节点,如果没有才去获取锁。这也是在新东方面试的时候没有回答上来的问题。
6.ReentrantLock和Synchronized的共同点和不同点
共同点:
两者都是可重入的独占锁,ReentrantLock在默认的情况下是非公平锁,Synchronized也是非公平锁
不同点:
1.Synchronized是在字节码指令进行加锁操作,ReentrantLock实在代码层面进行加锁操作
2.Synchronized锁的释放在执行完同步代码块或者同步方法的时候就会自动释放,ReentrantLock加完锁后得调用unlock方法手动释放,所以一般将unlock方法在finally里面来释放的
3.线程在获取Synchronized的同步锁的时候如果被阻塞了,会一直阻塞,不能被中断,不能干别的,而ReentrantLock在加锁的时候选择tryLock加锁,可以在没有获取到锁后直接返回,不用阻塞,或者lockInterruptibly方法加锁的话,可以在阻塞的过程中被中断
4.如果在加锁的代码块中运行发生异常,Synchronized会释放锁,ReentrantLock不会释放锁,所以一般将ReentrantLock锁的释放放在finally代码块里面。
5.ReentrantLock结合condition使用会比Synchronized和wait,notify结合使用的话更加灵活,ReentrantLock可以结合condition来唤醒和锁定特定的线程。