谈谈对java线程的理解(四)--------ReentrantLock锁

 

关于java中的锁,大家想必十分熟悉。提到锁,大家都会想到,哦,synchronized,wait,sleep,lock,notify 等等等等。当然对一些老鸟来说,这这些关键字或者方法,类都有一定的了解。对于一些新手来说可能只是处于那种不上不下,提到了,知道这么个东西,知道可以防止并发问题。说一个不太好笑的笑话,之前关于锁,我的理解就是synchronized,lock可以加锁,解锁,lock需要自己控制,而synchronized不需要,如果有人问我wait()可以用在lock中么?恐怕我的第一反应就是,为什么不可以?对啊,这不是等待么,加锁之后等待完全没问题啊。是啊没问题,可是就是不能用,【笑哭】,他们都不是一个体制内的,怎么混合使用?你让A公司的主管去命令B公司的员工试试?根本不可能么!

那么为什么不能用呢?我们来深入探讨下。

上一节,我们已经探讨了一下synchronized的使用以及简单的底层实现。知道synchronized是通过monitor来实现对方法,代码块,类等进行加锁,防止资源的抢占。那么wait()呢?其实,wait()也是根据monitor来的。当调用wait()方法时,首先会获取监视器,让线程进入等待队列并释放锁。然后其他线程调用notify或者notifyAll以后,会选择从等待队列中唤醒任意一个线程。而执行完notify方法之后,并不会马上唤醒线程,因为当前线程仍然持有这把锁,处于等待状态的线程无法获得锁,必须要等到当前的线程执行完monitorexit指令之后,也就是被释放之后,处于等待队列的线程就可以开始竞争锁了。

所以,wait()的作用有两个:释放锁和线程进入等待队列,二者都是和监事器相关所以要配合synchronized使用,不能和ReentrantLock混用。

我们来看下ReentrantLock,从定义看,public class ReentrantLock implements Lock, java.io.Serializable只是实现了一个Lock接口,并不是很特别复杂,一般一些类的实现继承了抽象类,然后又实现了几个接口,不同类之间都紧密的连在一起,看着都有些头晕。ReentrantLock 相比而言还是比较友好的哟。
既然是一个对象,我们使用的时候肯定要调用构造方法,从ReentrantLock中来看,构造方法有两个,一个是无参构造方法,一个是有参。

 public ReentrantLock() {
         // 无参构造方法,默认使用非公平锁
        sync = new NonfairSync();
    }


 public ReentrantLock(boolean fair) {
        // 有参构造方法,定义公平锁与分公平锁
        sync = fair ? new FairSync() : new NonfairSync();
    }
下面是非公平锁NofairSync中lock的实现,因为是非公平锁,所以上来就进行抢占锁的操作,使用乐
观锁CAS,抢占成功,当前线程就占有资源,否则就加入缓存队列。
static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;
    //进行加锁
    final void lock() {
        // 非公平,上来直接抢占锁,成功的话获取到所,直接可以执行
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }
    //尝试获取锁
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}
--

我们一步一步来看:点进去compareAndSetState方法,可以看到是进入了AQS(AbstractQueuedSynchronizer)类中的方法unsafe.compareAndSwapInt(this, stateOffset, expect, update); 最终进入了java unsafe包中的cas操作,就是如果stateOffset对应的字段是0的话,改成1,否则失败。那么stateOffset对应的是哪个字段呢?我们点进去看下就知道了。然后大家就看到了这段代码

static {
    try {
        stateOffset = unsafe.objectFieldOffset
            (AbstractQueuedSynchronizer.class.getDeclaredField("state"));
        headOffset = unsafe.objectFieldOffset
            (AbstractQueuedSynchronizer.class.getDeclaredField("head"));
        tailOffset = unsafe.objectFieldOffset
            (AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
        waitStatusOffset = unsafe.objectFieldOffset
            (Node.class.getDeclaredField("waitStatus"));
        nextOffset = unsafe.objectFieldOffset
            (Node.class.getDeclaredField("next"));

    } catch (Exception ex) { throw new Error(ex); }
}

也就是一个静态代码块,也就是AQS创建的时候就已经加载的,我们可以看到stateOffset对应的正是state字段!利用的是反射技术。也就是说,CAS操作是把state从0修改为1。

说到这里,我们来看下state,不然后面的看着可能有点晕。我们知道ReentrantLock或者说AQS是用来加锁,防止线程的竞争的,那么这个“锁”来自哪里呢?

其实也就是通过一个状态来控制,这个状态(state)为0,表示没有线程竞争当前资源,大于0表示有线程占用当前资源,每次线程抢占到锁,都会对状态(state)进行加一,(独占锁)其他线程无法抢占该资源。如果释放资源,则状态(state)减一,表示释放资源。当然state可以一直加一,表示可重入,然后释放的时候一直减一,直到资源释放,状态(state)归0。

所以ReentrantLocknt中的加锁,就是讲状态(state)改成1,抢占锁资源。

好了,我们继续。

如果抢占锁成功,当然是将当前线程设置为资源拥有者,一个set操作,不多说了

setExclusiveOwnerThread(Thread.currentThread())

如果抢占锁失败,执行acquire(1);进入这个方法,我们可以看到

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

我们一个一个来看;我们点击tryAcquire,进入了AQS中的tryAcquire方法,发现实现是抛出一个不支持的异常,当然这个不是最终的实现,据我从别的资料上看到的说,这个定义为方法而不是接口,主要是方便某些子类集成的时候,如果不需要这个方法,不用再单独实现了,当然如果需要肯定要自己实现。所以,我们点看这个方法的实现,可以看到非公平锁的一个实现。(idea的话,按住ctrl+alt,然后单击就能看到方法的实现);

我们可以看到进了java.util.concurrent.locks.ReentrantLock.NonfairSync#tryAcquire这个方法,具体的实现在nonfairTryAcquire中,下面我们来看下这个方法。

    final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread(); 
            int c = getState(); //获取state,就是控制锁的那个状态,这个是父类的方法
            if (c == 0) {      //state为0,表示资源空闲,可以进行资源抢占,使用cas乐观锁抢占
                  //acquires传来的参数是1,也就是把0改1,这个不多说了,看上文
                if (compareAndSetState(0, acquires)) {  
                     //抢占锁成功,将当前线程设置为资源拥有者,并返回true,表示加锁成功
                    setExclusiveOwnerThread(current);
                    return true;
                }
            } 
           // 可重入判断,表示当前线程是资源拥有者,再次加锁处理(锁中锁,连续多个lock)
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;  // state 就加一
                if (nextc < 0) //  超出最多限制了,state是int类型,如果超出最大数,会获得负数,一般不会的
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);  //把状态设置进去
                return true;  //再次获取锁(重入锁)成功
            }
            // 否则获取锁失败
            return false;
        }

这个方法总的来说就是当前线程先尝试获取锁,不成功的话就判断是否已经拥有锁,然后进行再次加锁(重入),否则就是加锁失败。我们接着看下一个方法acquireQueued(addWaiter(Node.EXCLUSIVE) ,      acquireQueued(addWaiter(Node.EXCLUSIVE),arg)这个方法ReentrantLock并没有自己实现,而是使用AQS的实现,具体来看下。

先看下 addWaiter(Node.EXCLUSIVE),Node.EXCLUSIVE定义就是null

private Node addWaiter(Node mode) {   // mode传来的参数是null 
        Node node = new Node(Thread.currentThread(), mode); //以当前线程为参数,建立一个节点
        Node pred = tail;
        //队列尾部不等于空,将当前节点插入队列尾部
        if (pred != null) {
            node.prev = pred;
             // cac操作,插入并返回
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //插入失败,使用一个无限循环进行插入,直到插入成功
        enq(node);
        return node;
    }

 // enq 直接复制过来了在这里看下,就是一个自旋处理

private Node enq(final Node node) {
        for (;;) {  // 一直循环,直到break或者return,下面就是插入队列尾部的一个操作,不多说了
            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(添加等待者)就是当前线程没有抢占到资源,需要去排队等待,于是将当前线程加入到等待队列(双向链表)中。AQS处理锁的思想类似于我们早上买早餐,轮到你了,你就开始做事情(告诉营业员要什么),没轮到就在后面排队,等到买东西的人让出了位置,队伍就短了一点,直到你可以买早餐。

既然加到队列里面去了,那不是就结束了么?怎么还有个方法啊?是啊,正常情况下是结束了,但是突然一个人看了下时间,唉?时间不够了,今天不吃早餐了,然后他就离开了,这时候队伍不是少了一个人么?是不是要更新队列?同理,这个线程的阻塞队列也需要进行更新处理的。

final boolean acquireQueued(final Node node, int arg) {  //node是尾节点, arg传来的参数是1,这个我们要心里有数哟
    boolean failed = true;
    try {
        boolean interrupted = false;  //线程中断标识
        for (;;) {  //无限循环,直到返回数据
            final Node p = node.predecessor(); //获取当前节点的前一个节点
             //如果前一个节点是头结点,说不定占有资源的线程已经干完活了,我试下能不能拿到资源
            if (p == head && tryAcquire(arg)) {  //成功的话,那当前线程就变成了头结点
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;  // 返回false,线程不中断
            }
            //获取资源失败之后等待,并且中断对线程设置中断标志,等待唤醒
            if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())  
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

又来了几个方法tryAquire(1),shouldParkAfterFailedAcquire(p, node) ,parkAndCheckInterrupt() 一个一个来吧,我们先来看下,tryAquire这个方法有种实现,我们来看下非公平锁的实现(我们说的就是非公平锁哦),就是下面这个方法,呃,上文已经讲过了。

final boolean nonfairTryAcquire(int acquires) { //入参是1要明确
   // 代码就不复制过来了,上文有讲解的
}

开始下个方法shouldParkAfterFailedAcquire,一起来看下,汗,看了下源码,又牵涉到新东西了,节点的状态waitStatus,就是说后面既然在等待,那线程就不用进行操作了啊,等占有资源的线程干完活了,再唤醒你不就好了,省的浪费CPU性能,于是,就有了一个状态waitStatus的处理了,这个是AQS里面的,大家有空可以先看下,下次再续,本想简单的分享下,没想到这么长。

感觉还可以的话,给个赞呗~

No  sacrifice,no victory!!!

猜你喜欢

转载自blog.csdn.net/zsah2011/article/details/108063824