ReentrantLock原理&源码解析(公平锁,非公平锁,重入)

1. 简介

ReentrantLock,可重入锁,指的是同一个线程,外层函数获得锁之后,内层递归函数仍然能获取该锁的代码;同一个线程,在外层函数获得锁之后,在进入内层方法时,会自动获取锁;
使用demo详见另外一篇博文《java锁类型详解》
同时ReentrantLock内部也实现了公平与非公平两种模式。

本文着重讲解ReentrantLock的源码。看看如何实现可重入,公平与非公平。
先上个使用demo,来个直观感受:

 //声明一个可重入锁
    Lock lock = new ReentrantLock();
    
    public void get() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getId() + "\t invoked set()");
            //调用下一个加锁的方法
            set();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void set() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getId() + "\t ######invoked set()");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

线程调用get()方法,拿到锁后,再调用加了锁的set()方法, 可以直接获取到锁。
可以看到,主要方法就是lock.lock();和unlock;

public void lock() {
   sync.lock();
}

lock()是sync内部类的抽象方法,是不是感觉有点混乱,下面我们先看看ReentrantLock代码结构。

2.代码结构

先看代码结构:
在这里插入图片描述
ReentrantLock内部包含3个静态内部类:

  • Sync :同步类,继承自AQS(AbstractQueuedSynchronizer,此类比较重要,但我们不单独讲解,我们结合具体的实现类中的方法说明)
  • NonfairSync :非公平锁
  • FairSync :公平锁
    在这里插入图片描述在这里插入图片描述
    内部类继承关系如上图。

3.构造方法

如果想使用ReentrantLock,必然先创建对象,我们看下2个简单构造方法:

//无参数构造方法,直接new了一个非公平锁
//因此ReentrantLock默认是非公平锁
public ReentrantLock() {
   sync = new NonfairSync();
}
//也可以指定锁类型
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

所以,再我们new ReentrantLock();的时候,默认使用的是非公平锁,lock()方法也是非公平锁的lock。继续往下看。

4.非公平锁NonfairSync

既然默认是非公平锁,我们就先从NonfairSync开始吧。
此内部类总共就俩方法:
在这里插入图片描述

4.1 lock()方法

我们先看常用的lock()方法:

final void lock() {
  //compareAndSetState是AQS中的,原理就是乐观锁cas
  //作用就是尝试获取锁,获取到,就在内存的某个位置标记1
  if (compareAndSetState(0, 1))
        //如果获取到了锁,就标记此线程独占了此锁
        setExclusiveOwnerThread(Thread.currentThread());
    else
    	//直接没有获取到锁,则走公平锁的的拿锁逻辑,内部需要排队
        acquire(1);
}

说明:当new了一个锁lock后,这个lock往往是全局变量,供多个线程使用:Lock lock = new ReentrantLock();
1.当一个线程thread调用lock方法时,因为是非公平锁,所以会直接尝试获取(抢)锁,成功后标记线程占用此锁,
2.如果失败,则调用acquire(1);进行排队;

4.2 acquire()

下面看看acquire(1)的实现:

public final void acquire(int arg) {
   if (!tryAcquire(arg) &&//再次尝试抢锁
   		//加入队列排队
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        //中断当前线程
        selfInterrupt();
}

此方法是AQS中的方法,其中if条件比较复杂,分开说:

  • tryAcquire(arg) :再次尝试抢锁
  • addWaiter(Node.EXCLUSIVE) :创建节点,加入队列
  • acquireQueued() :再次判断是否真的需要排队

4.2.1 tryAcquire(arg)

重点看tryAcquire(arg),这是在公平锁与非公平锁中自己实现的,并且是各自单独实现的:

protected final boolean tryAcquire(int acquires) {
   return nonfairTryAcquire(acquires);
}
//父类Sync中实现
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    //此锁被某个线程拥有的次数(一个线程可能会多次获取此锁),开始肯定是0
    int c = getState();
    if (c == 0) {
    	//compareAndSetState(0, acquires):再一次直接抢锁,之前在lock()开始的时候进行过此逻辑
    	//抢到了锁,则返回ture
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //如果状态不是0(说明可能是此线程之前在别的方法中可能已经拿到了此锁)
    //进入此if,进一步判断是不是当前线程,返回ture,这也是可重入锁的含义:此线程别的A方法已经拿到锁,那么方法B可直接获取到锁
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
        //int类型值溢出,跑异常
            throw new Error("Maximum lock count exceeded");
        //更新此锁的state值    
        setState(nextc);
        return true;
    }
     //如果尝试获取锁失败,返回false
    return false;
}

步骤总结:
1.先通过state字段,看看这个锁是否被人持有,如果没有人持有,不管有没有人排队,就直接尝试枪锁;
2.如果是被人持有,则看看持有人是不是自己,如果是自己,就直接拿到此锁,这也是可重入核心原理实现

4.2.2 addWaiter

if (!tryAcquire(arg) &&
//加入队列排队
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)){}

继续看addWaiter:

//加入等待队列中,创建一个新的节点,将此节点放置于队列尾部,并将此节点返回
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    //拿到当前队列中的尾部节点
    Node pred = tail;
    if (pred != null) {
    //如果尾部节点不是null,那么将新节点指向尾部节点
        node.prev = pred;
        //cas,重新将新节点设置为尾部节点
        if (compareAndSetTail(pred, node)) {
            //尾部的前一个节点指向此尾部节点
            pred.next = node;
            return node;
        }
    }
    //如果尾部节点是null,进入死循环,初始化头部和尾部节点,并把当前节点连接到tail后边
    enq(node);
    return node;
}

//创建头部节点,并让尾部==头部
//将新节点链接到尾部
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
        //初始化头部节点和尾部节点
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

步骤总结:
addWaiter的主要作用就是new一个当前线程的节点,并拼接到tail节点后边,同时让tail也指向此节点;最后返回此节点。
但是当tail为空时,要先初始化tail。

4.2.3 acquireQueued()

if (!tryAcquire(arg) &&
//加入队列排队
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)){}
继续看:

 //是否真的需要排队
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)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            //走到这,说明仍然没获取到锁,仍然可能需要排队
            //那么就继续判断它前边那个人的状态
            if (shouldParkAfterFailedAcquire(p, node) &&
            //进行线程中断操作
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

步骤总结:
此方法的主要作用是,判断当前线程是否真的需要排队;因为addWaiter已经将当前线程的node排到了队列尾部;
但是,如果它的前一个节点就是head节点,那也是不需要排队的,因为它已经算是在最前边了;
如果前边不是头节点,那么也不一定需要排队等待,因为前边的人可能正好处理完了,要把锁交给它了;最后如果这些条件都不符合,那么就中断等待别人唤醒。

5.公平锁FairSync

5.1 lock()方法

final void lock() {
   acquire(1);
}

公平锁的lock和非公平很相似,只是少了抢锁的步骤,可以对比4.1节

5.2 tryAcquire()方法

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    //此锁被若干线程拥有的次数,开始肯定是0
    int c = getState();
    if (c == 0) {
    	//hasQueuedPredecessors():队列中是否有别的线程在此线程前边排队?没有则继续判断
    	//compareAndSetState(0, acquires):再一次直接抢锁,之前在lock()开始的时候进行过此逻辑
    	//上边这俩条件都满足,说明抢到了锁,返回ture
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //如果状态不是0(说明此线程之前在别的方法中可能已经拿到了此锁)
    //进入此if,就会返回ture,这也是可重入锁的含义:此线程别的A方法已经拿到锁,那么方法B可直接获取到锁
    else if (current == getExclusiveOwnerThread()) {
        //有线程拿到此锁,则计数+1
        int nextc = c + acquires;
        if (nextc < 0)
        //int类型值溢出,跑异常
            throw new Error("Maximum lock count exceeded");
        //更新此锁的state值
        setState(nextc);
        return true;
    }
    //如果尝试获取锁失败,返回false
    return false;
}

步骤总结:
1.先通过state字段,看看这个锁是否被人持有,如果没有人持有,又没有人在前面排队,那么就直接尝试枪锁;
2.如果是被人持有,则看看持有人是不是自己,如果是自己,就直接拿到此锁,这也是可重入的核心实现原理

公平锁的tryAcquire()与非公平锁基本一致,只有一行代码不同:hasQueuedPredecessors()
公平锁获取锁的判断条件中多了hasQueuedPredecessors():判断队列中是否有别的线程在此线程前边排队

//判断是是否有人再前边排队
public final boolean hasQueuedPredecessors() {
     Node t = tail; // Read fields in reverse initialization order
     Node h = head;
     Node s;
     //头节点不等于尾部节点
     //头节点的下一个节点,不能是null,也不能是当前线程,表示前面有别人在排队,这也是公平锁的核心理念:有别人在前边,我不能插队
     return h != t &&
         ((s = h.next) == null || s.thread != Thread.currentThread());
 }

6.unlock()方法

加完锁,最后都是要释放锁的。

public void unlock() {
   sync.release(1);
}
public final boolean release(int arg) {
   if (tryRelease(arg)) {//释放锁,设置state=state-1
       Node h = head;
       if (h != null && h.waitStatus != 0)
       //通知后续节点拿锁
           unparkSuccessor(h);
       return true;
   }
   return false;
}
protected final boolean tryRelease(int releases) {
    //将state状态-1
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    //state是一个线程加锁的次数,要想释放,必须减到0;比如加了两次锁,就必须有两次释放锁的动作
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

步骤总结:
释放锁,就是将state(一个线程加锁的次数)减到0,然后通知队列下一个线程节点。

到这里,ReentrantLock的核心方法和原理,基本理清楚了,其他方法细节,后续再补充吧。

7.lockInterruptibly()方法

lockInterruptibly()比较特殊,他与lock()方法的区别是:
lock优先考虑获取锁,获取到锁之后,才会响应中断;
lockInterruptibly()优先响应中断,也就是即使没获取到锁,进入了等待状态,依然可以响应thread.interrupt()中断方法,停止等待。

发布了62 篇原创文章 · 获赞 29 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/csdn_20150804/article/details/97973032