Javaスレッドの理解について話す(4)-------- 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();

それを1つずつ見ていきましょう。tryAcquireをクリックしてAQSにtryAcquireメソッドを入力したところ、実装がサポートされていない例外をスローすることがわかりました。もちろん、これは最終的な実装ではありません。他の資料から見たところによると、この定義これは、主に特定のサブクラスの統合を容易にするためのインターフェイスではなくメソッドです。このメソッドが必要ない場合は、個別に実装する必要はありません。もちろん、必要な場合は、実装する必要があります。あなた自身。したがって、このメソッドの実装を見ると、不公平なロックの実装がわかります。(アイデアについては、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にあります。チェックアウトできます。時間があれば、次回も続けて、簡単に共有したかったのですが、そんなに長くなるとは思っていませんでした。

よろしければお褒めの言葉をお願いします〜

犠牲も勝利もありません!!!

おすすめ

転載: blog.csdn.net/zsah2011/article/details/108063824