互斥锁的实现细节

***首先,一个互斥锁要实现什么功能?***

一个互斥锁需要有阻塞和唤醒功能,实现阻塞和唤醒功能需要哪些要素?
①需要有一个标记锁状态的state变量。
②需要记录哪个线程持有了锁。
③需要有一个队列维护所有的线程。
另外,state和队列中为了实现线程安全都用到了CAS。
有了以上三个条件,可以实现对线程的阻塞和唤醒。

***那么,Java中是如何实现一把互斥锁的?***

首先,因为所有的锁基本都要实现重入功能,那么Java中的互斥锁由ReentrantLock类来定义,也就是可重入锁,也就是说ReentrantLock类中的锁是互斥锁。(虽然ReentrantLock很重要,但是ReentrantLock里面并没有代码逻辑,ReentrantLock里面的具体实现就是在NonfairSync和FairSync中,ReentrantLock就是构造方法里面也是 new 一个 NonfairSync或者FairSync实例,还有lock和unlock是调用的sync.lock 和 sync.release,而sync中的lock是抽象方法,又由NonfairSync和FairSync分别实现,而sync.release则是sync继承的AbstractQueuedSynchronized中的方法,所以unlock并没有公平和非公平两种实现 )。
先介绍一下ReentrantLock中的继承结构,ReentrantLock实现了Lock接口,实现了Lock接口中的lock() , lockInterruptibly() , unlock() , tryLock() , tryLock(long time, TimeUnit unit) , Condition newCondition();其中 lockInterruptibly() 和 tryLock(long time, TimeUnit unit) 都会抛出InterruptedException。而且 ReentrantLock 引用了 Sync 抽象内部类,Sync 又分别被 NonfairSync 和 FairSync 实现,Sync还继承了AbstractQueuedSynchroinzer 队列同步器,AbstractQueuedSynchronized又继承了AbstractOwnerSynchronize。
介绍完ReentrantLock我们继续介绍,ReentrantLock是如何实现互斥锁的?

1.State,state定义在AbstractQueuedSynchronized中,用来记录线程重入了多少次这个锁。

2.记录哪个线程持有了锁,在AbstractOwnerSynchronized中定义了Thread类型的 exclusiveOwnerThread(独占拥有者线程)来记录锁的拥有线程。

3.队列,在AbstractQueuedSynchronized中主要就是定义了一个用双向链表实现的队列,用来存储线程,实现线程管理。

如何通过这些条件实现阻塞和唤醒?(涉及公平和非公平的)

阻塞是在什么情况下发生的?线程抢锁,抢锁成功访问资源,抢锁失败阻塞。唤醒?从阻塞队列中被唤醒后尝试拿锁,访问资源,没拿到锁会再次被阻塞(两种唤醒方式unpark(Thread t)、t.interrupt()两种方式都需要其他的线程执行)。 

什么情况下线程抢锁?多个线程都调用了lock方法,都在尝试给自己上锁,尝试之后没有抢到会怎么样?只能阻塞。下面我们来看NonfairSync和FairSync对最终对lock方法的不同实现:

    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

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

        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        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;
        }
    }

我们可以看到,不管是NonfairSync或者FairSync中的lock方法最后都要执行acquire(int arg)方法,acquire翻译过来就是获取的意思,就是获取锁,acquire是继承自AbstractQueuedSynchronized的方法,代码如下:

public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
}

我们其实可以从方法名中看出acquire方法内部是获取锁的逻辑,但是非公平锁很不讲道理的在还没有进入acquire方法正式抢锁之前就已经尝试抢过一次锁了。进入acquire方法中,首先进入一个tryAcquire()尝试抢锁方法,这个方法在AbstractQueuedSynchronized中有定义,但是被NonfairSync和FairSync分别重写了,其中NonfairSync的tryAcquire方法中又返回了Sync中的nonfairTryAcquire方法,表明,NonfairSync类中的tryAcquire方法的主体逻辑写在了Sync类中,FairSync类中的tryAcquire方法中的逻辑还是正常的在自己的类中。

回来,我们继续说tryAcquire()方法有一个返回值,就是获取到锁返回true,没有获取到锁返回false,后面的acquireQueued方法是一个阻塞逻辑。if里面的逻辑就是,如果抢锁失败,返回false,执行acquireQueued()方法阻塞当前线程,如果抢锁成功就已经完成了目标不执行后面的阻塞逻辑。那么,tryAcquire(int arg)方法中arg总是1,意味着每抢锁成功一次加一,tryAcquire中的总体逻辑是,如果state值为0,那么在非公平锁中直接尝试设置state为1,设置成功就将标记独占者线程的Thread类型属性改为当前线程,完成抢锁,成功获得标记,没有被阻塞。如果state值不为0,那么就使state加1并且更新state的值,更新state值之前如果发现加一之后state小于0,则抛出锁的最大数量已经被超出了错误,一般不会发生,因为上限有20多亿(超过上限加一,变为负数,计算机底层二进制计算原理,应该是首位符号位变了);而在公平锁中,在state===0 时需要判断当前线程是否在阻塞队列头,如果当前线程在队列头,或者队列为空,则该线程可以抢锁。state不为零时和NonfairSync一样判断一下当前线程是被标记的独占者线程后,如果是就可以直接操作state,重入时不需要判断阻塞队列的情况。

获取锁失败之后,线程进入acquireQueued方法的逻辑中,acquireQueued方法中有一个addWaiter方法先将线程加入阻塞队列的最后一个节点,加入阻塞队列后并不会发生阻塞,只是记录了将线程记录到队列里面,等到线程进入acquireQueued方法才会阻塞。acquireQueued方法的内部的主体逻辑就是一个for死循环的内容,循环中首先判断线程所在节点是不是队列中的第一个节点,方法里用的是判断当前节点的前驱节点是不是head节点,如果是,则表明该节点位于队列第一个,则尝试获得锁,如果获得锁成功,就将该节点设置为头结点(也就是出队列,使head指针右移,并且是节点的Thread属性设置为null,也就是head还是指向了一个空节点),然后把p.next设置为null便于GC,最后返回一个布尔值判断该线程是否被中断过。如果线程没有获取到锁,或者根本不在队列第一位,那么直接跳到一个if()逻辑里,里面有两个方法,shouldParkAfterFailedAcquire(...)判断获取锁失败后应不应该阻塞,如果应该阻塞,则执行parkAndCheckInterrupt()方法,parkAndCheckInterrupt()方法里的逻辑也很简单,直接调用LockSupport.park(this)阻塞该线程,然后返回一个Thread.interrupted(),判断线程是否被中断过,如果被中断过,返回true,进入if下面的逻辑,设置中断标志值为true,表示该线程被中断过,acquireQueued方法就会返回true,就会执行selfInterrupt方法中断自己,自己的状态就会编程已被中断过状态;如果该线程没有被中断过,则Thread.interrupted()返回false,不执行if下面的逻辑中断标志值就为false,那么acquireQueued方法就会返回false,不会使用selfInterrupt()方法,也就不会把自己设置为已被中断过状态。之所以要自己中断自己是因为,线程被阻塞期间即使被中断了,也没有完成把自己的状态设置为已被中断过的状态。

以上是对lock()方法的介绍,下面我们来看看unlock() 方法的实现,lock()方法是获取锁,谁获取到锁谁访问资源,unlock()方法是释放锁,只有拥有锁的线程才能释放锁,否则会产生IllegalMonitorStateException;unlock()方法中返回了sync.release(1),而sync.release(1)是从AbstractQueuedSynchronized类中继承的方法。release(int arg)方法主要逻辑由两个方法执行,一个是tryRelease(int arg)方法和unparkSuccessor(Node node)方法,tryRelease(int arg)尝试释放锁,每调用一次state减一,如果state==0,那么使用set ExclusiveOwnerThread(Thread thread)方法,将exclusiveOwnerThread设置为null,表示没有线程持有该锁。释放锁成功则返回true,就绪执行if中的逻辑,里面逻辑大致为如果队列中有节点的话,就调用unparkSuccessor(Node node)唤醒该方法。也就是唤醒后继节点,使其尝试获取锁。

再来看一个方法lockInterruptibly()实现分析,其实和lock的几乎一样,唯一的区别就是可被interrupt()方法唤醒,怎么实现的呢?就是在parkAndCheckInterrupt()方法那里当Thread.interrupted()返回为true进入if下的逻辑时,把lock时设置的中断标志变量,改为抛出一个异常,那么就实现了可被中断的作用。

最后是一个tryLock方法,ReentrantLock中的tryLock方法返回一个sync.nonfairTryAcquire(1)方法,sync.nonfairTryAcquire(int arg)会尝试获取锁,如果没有获取锁成功,会返回一个false,获取成功就会返回true,sync.nonfairTryAcquire(int arg)中只尝试获取锁一次,并不循环,然会返回true或false,再由tryLock方法返回这个值。需要注意的是非公平锁的lock其实也用了这个方法获取锁,但是如果返回false,lock中的代码会使的线程继续进入acquireQueued方法阻塞。

 

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值