Talk about the understanding of java thread (four) -------- ReentrantLock lock

 

关于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())

If the lock preemption fails, execute acquire(1); enter this method, we can see

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

Let's look at them one by one; we click on tryAcquire, enter the tryAcquire method in AQS, and find that the implementation throws an unsupported exception. Of course, this is not the final implementation. According to what I have seen from other materials, this definition It is a method instead of an interface, which is mainly to facilitate the integration of certain subclasses. If this method is not needed, it does not need to be implemented separately. Of course, if you need it, you must implement it yourself. So, if we look at the implementation of this method, we can see an implementation of unfair locks. (For idea, hold down ctrl+alt and click to see the implementation of the method);

We can see that the java.util.concurrent.locks.ReentrantLock.NonfairSync#tryAcquire method is entered. The specific implementation is in nonfairTryAcquire. Let’s take a look at this method.

    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;
        }

In general, this method is that the current thread tries to acquire the lock first. If it is unsuccessful, it will determine whether it already owns the lock, and then perform the lock again (re-entry), otherwise the lock fails. Let's look at the next method acquireQueued(addWaiter(Node.EXCLUSIVE), acquireQueued(addWaiter(Node.EXCLUSIVE),arg). This method ReentrantLock is not implemented by itself, but is implemented using AQS. Let's look at it in detail.

First look at addWaiter(Node.EXCLUSIVE), Node.EXCLUSIVE is defined as 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;
                }
            }
        }
    }

Combined, in fact, addWaiter (add waiter) means that the current thread has not preempted resources and needs to wait in line, so the current thread is added to the waiting queue (doubly linked list). AQS’s idea of ​​handling locks is similar to when we buy breakfast in the morning. When it’s your turn, you start to do things (tell the salesperson what you want), line up at the back when it’s not your turn, and wait until the buyer vacates the place, and the line is short. A little bit until you can buy breakfast.

Now that it's added to the queue, isn't it over? Why is there another way? Yes, it is over under normal circumstances, but suddenly one person looks at the time, huh? Time is running out. I skipped breakfast today, and then he left. Wasn't the team missing one person at this time? Do you want to update the queue? In the same way, the blocking queue of this thread also needs to be updated.

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);
    }
}

 

 

There are several methods tryAquire(1), shouldParkAfterFailedAcquire(p, node), parkAndCheckInterrupt() one by one. Let’s take a look first. There is a kind of implementation of tryAquire. Let’s take a look at the implementation of unfair locks (our It's talking about unfair locks), it's the following method, uh, I've already talked about it above.

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

Start the next method shouldParkAfterFailedAcquire, let’s take a look, sweat, read the source code, and involve new things, the status of the node waitStatus, that is to say, since it is waiting later, the thread does not need to operate, wait for the resource to be occupied After the thread finishes its work, it won’t be enough to wake you up. It saves the waste of CPU performance, so there is a status waitStatus processing. This is in AQS. You can check it out when you have time and continue next time. , I wanted to share it briefly, but I didn't expect it to be so long.

If it feels okay, give me a compliment~

No  sacrifice,no victory!!!

Guess you like

Origin blog.csdn.net/zsah2011/article/details/108063824