首页投稿(暂停使用,暂停投稿)Java学习笔记代码改变世界

ReentrantLock 锁机制(非公平锁)源码

2016-10-08  本文已影响443人  Skymiles

ReentrantLock和synchronized一样加锁方式,独占的获取同步的对象锁。

    1. 简介

    ReentrantLock内部由Sync类实例实现其中ReentrantLock.FairSync,ReentrantLock.NonfairSync两个实现,也就是常说的公平锁和不公平锁。

而Sync继承于AbstractQueuedSynchronizer。AbstractQueuedSynchronizer这个类真的很难也很复杂,是构建锁以及实现其他相关同步类的基础框架。本篇文章只能说是对自己看这个类的一点点理解和记录,若有错,请批评指正。

    2. ReentrantLock类的lock()方法

    由于锁Lock的实现都是委托给AbstractQueuedSynchronizer来实现的。因此,就将分析ReenterantLock类中如何获取锁和如何释放锁来理解。

public voidJava.util.concurrent.locks.ReentrantLock.lock()

如果该锁没有被另一个线程保持,则获取该锁并立即返回,并将锁的保持计数器设置为1.

如果当前线程已经保持该锁,则将保持计数加1,并且该方法立即返回。

如果该锁被另一个线程保持,则出于线程调度的目的,禁用该线程,并且在获得锁之前,该线程一直处于休眠状态,此时锁保持计数被设置为1.

    保持计数就是AQS类的state变量。

默认ReentrantLock构造器

public ReentrantLock() {

    sync =new NonfairSync();

}

默认是非公平锁,看看NonfairSync类下的lock方法:

final void lock() {

    if (compareAndSetState(0,1))

        setExclusiveOwnerThread(Thread.currentThread());

    else

        acquire(1);

}

    compareAndSetState(0, 1) 这个是尝试获取锁,把state的状态从0改为1表示取得锁。这个方法原理是CAS,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做!

    setExclusiveOwnerThread()设置获取锁的线程就是当前线程.

    具体调用的是:

protected final boolean compareAndSetState (int expect, int update) {

    return unsafe.compareAndSwapInt (this, stateOffset, expect, update);

}

    unsafe的compareAndSwapInt方法是native的.

但是我们更关注的是,当前线程申请锁不成功的时候是怎么做的.可以看到是AQS中的acquire(1);

public final void acquire(int arg) {

//先尝试获取锁,如果获取锁失败了,acquireQueued则执行

    if( !tryAcquire(arg) && acquireQueued( addWaiter(Node.EXCLUSIVE), arg))

        selfInterrupt();

}

后面的方法。注意&&,前面为true后面才执行。获取锁失败后,会将该线程加入等待队列

    这个看起来比较复杂,我们分解以下4个步骤。

1、如果tryAcquire(arg)成功,那就没有问题,已经拿到锁,整个lock()过程就结束了。如果失败进行操作2。

2、调用addWaiter方法:将当前线程创建一个独占节点(Node)并且此节点加入CHL队列末尾。进行操作3。

3、自旋尝试获取锁,失败根据前一个节点来决定是否挂起(park()),直到成功获取到锁。进行操作4。

4、如果当前线程已经中断过,那么就中断当前线程(清除中断位)。

下面我们对acquire方法中调用的其它方法一一进行分析。

    tryAcquire(acquires)

protected final boolean tryAcquire(int acquires) {

    return nonfairTryAcquire(acquires);

}

final boolean nonfairTryAcquire (int acquires) {

    final Thread current = Thread.currentThread(); 

    int c = getState();  //对于AQS存在一个state来描述当前有多少线程持有锁

    /*

        如果c等于零,则没有线程持有锁,则将锁给当前线程即可

        如果c不等于,说明当前线程已经获取了锁,这里是当前线程再次要获得锁,所以state           要继续+1

    */

    if(c ==0) {

        if(compareAndSetState(0, acquires)) {

            setExclusiveOwnerThread(current);

            return true;

        }

    }

    else if (current == getExclusiveOwnerThread()) { //判断当前线程是否为AQS的独占线程

        int nextc = c + acquires;

        if( nextc < 0 ) // overflow 

            throw new Error("Maximum lock count exceeded");

        setState(nextc);

        return true;

    }

    //获取锁失败,返回false。

    return false;

    tryAcquire的逻辑是这样的, c = getState() 就是当前没有锁竞争的时候,会再尝试去获得锁.

current == getExclusiveOwnerThread()):当前线程已经获取锁了,那么锁的记数加1.

addWaiter(mode)

    如果tryAcquire没有成功,  就执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

    addWaiter是把线程和线程的状态信息封装到一个node对象,加入CHL阻塞链表,Node封装了各种线程状态:

static final int CANCELLED = 1; //这个状态说明该节点已经被取消。

static final int SIGNAL = -1; //这个状态说明该节点后续有阻塞的节点 。

static final int CONDITION = -2; //表明该线程被处于条件队列,就是因为调用了Condition.await而被阻塞

static final int PROPAGATE(-3); //传播共享锁

0:0代表无状态

    其实就是把当前线程放到一个链表的末尾去.具体怎么放有点讲究,而且用到了无限循环,也就是说,一定要把线程放进链表的!

    static final Node EXCLUSIVE = null; //独占节点模式

    static final Node SHARED = new Node(); //共享节点模式

    addWaiter(mode)中的mode就是节点模式,也就是共享锁还是独占锁模式。

privateNode addWaiter(Node mode) {

    //当前线程节点,线程的状态信息封装到一个node对象。

    Node node =new Node(Thread.currentThread(), mode);

    Node pred = tail;

    //判断有没有尾节点(也就是前面是否有等待线程)。如果有尾节点,则将当前线程的节点插入到队列的尾部,也就是将当前线程变成尾节点。

    if(pred != null) {

        node.prev = pred;

        //CAS的操作。

        if(compareAndSetTail(pred, node)) {

            pred.next = node;

            return node;

        }

    }

    //如果没有尾节点,说明前面还未有等待线程。调用下面的方法,创建等待队列

    enq(node);

    return node;

}

总而言之,addWaiter的目的就是通过CAS把当前现在追加到队尾,并返回包装后的Node实例。

acquireQueued(node,arg)

//这个方法是不断地获取锁,直到成功的获取锁,或者阻塞当前线程

acquireQueued也是个无限循环。就是说要么获取到锁,要么中断当前线程。

1. acquireQueued方法在无限循环内获取前继节点,判断前继节点是否为head,是就再尝试     获取锁,之后前继节点dequeue出队,node成为head。

2. 前继节点p != head 或者 前继节点p == head但是tryAcquire失败了,那么应该阻塞当前线     程等待前继唤醒。阻塞之前会再重试一次,还需要设置前继的waitStaus为SIGNAL。

3. 调用shouldParkAfterFailedAcquire方法,后面的方法是对当前线程进行阻塞并且判断是       否中断。这里注意的是,如果一个线程在等待锁期间这个线程被中断了,这里会将               interrupted赋为true,但是并不return。这个还一直进行for循环,知道这个线程获得了锁,     所以lock()方法不能立即响应中断,必须等线程获得了锁才可以响应中断。对应的可以立       即响应中断的方法为lockInterruptibly()方法。

//阻塞当前线程

private final boolean parkAndCheckInterrupt() {

    //阻塞当前线程

    LockSupport.park(this);

    return Thread.interrupted();

unLock()方法

public void unlock() {

    sync.release(1);

非公平锁release方法 非公平锁tryRelease方法
上一篇下一篇

猜你喜欢

热点阅读