光輝歲月

导航

 

  什么都无法舍弃的人,什么也改变不了!!!

 

  1.ReentrantLock特性

  A:支持公平加锁和非公平加锁方式

  B:独占模式加锁

  C:支持手动解锁

  D:支持锁的重入

 

  2.ReentrantLock的核心原理

  A:自旋——等待加锁和解锁

  B:park——线程阻塞(第一次尝试阻塞不会直接阻塞,而是先标记head节点的waitStatus为-1,为了下一个节点状态为可唤醒状态)

  C:cas——保证加锁的原子性

  D:队列——先进先出的数据结构,保证加锁的公平性(双向链表实现)

 

  3.ReentrantLock结构

  

 

  4.什么是CAS操作

  Conmpare And Swap 比较与交换:

/**
     * CAS head field. Used only by enq.
     */
    private final boolean compareAndSetHead(Node update) {
        return unsafe.compareAndSwapObject(this, headOffset, null, update);
    }

    /**
     * CAS tail field. Used only by enq.
     */
    private final boolean compareAndSetTail(Node expect, Node update) {
        return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
    }

    /**
     * CAS waitStatus field of a node.
     */
    private static final boolean compareAndSetWaitStatus(Node node,
                                                         int expect,
                                                         int update) {
        return unsafe.compareAndSwapInt(node, waitStatusOffset,
                                        expect, update);
    }

    /**
     * CAS next field of a node.
     */
    private static final boolean compareAndSetNext(Node node,
                                                   Node expect,
                                                   Node update) {
        return unsafe.compareAndSwapObject(node, nextOffset, expect, update);
    }

  上述代码,都是AQS中的CAS方法,调用的都是Unsafe类中的方法,都是native方法,原理就不深纠了,这个是CPU硬件实现。

  一共四个参数,第一个参数是需要CAS操作的对象(目标对象),第二个参数目标对象的属性偏移量(jvm的安全手段,获取的是属性在元空间的地址段),第三个参数比较值(与目标对象的属性的内存值做比较的值),第四个参数期望值(如果目标对象的属性的值,跟第三个参数一样,那么就将目标对象的属性的内存值改为期望值)。

  这些方法在高并发场景下,只有一个会更改成功,保证了原子操作。

 

  5.AQS的CLH队列实现

private Node addWaiter(Node mode) {                //入队列方法
        Node node = new Node(Thread.currentThread(), mode);    //构建一个新的节点,绑定当前线程,第二个参数此时是null,也就是说这个节点的nextWaiter是null
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;                        //获取尾节点
        if (pred != null) {            //如果队列已经初始化,此时尾节点不为空
            node.prev = pred;            //把新建的节点的上一个节点指向尾节点(末尾插入队列)
            if (compareAndSetTail(pred, node)) {  //cas操作,把尾结点设置成这个新的节点
                pred.next = node;            //如果成功了,在把原来的尾节点的下一个节点指向这个新的节点,然后把这个节点返回
                return node;
            }
        }
        enq(node);      //能跑到这来说明此时队列还没初始化,亦或者说是上面的尾插入失败了
        return node;
    }

   在来看enq方法(讲节点入队列,如果节点还没初始化,先进行初始化):

private Node enq(final Node node) {
        for (;;) {            //先来一个自旋
            Node t = tail;       //获取尾节点
            if (t == null) { // Must initialize    如果为节点是空的,说明此时队列还未初始化
                if (compareAndSetHead(new Node()))      //cas操作,设置一个新的头结点
                    tail = head;              //讲尾节点也设置成这个新的头结点
            } else {
                node.prev = t;                //跑到这来说明此时队列初始化已经完成;将参数目标节点的上一个节点指向尾节点
                if (compareAndSetTail(t, node)) {     //cas操作设置尾节点为参数节点
                    t.next = node;              //如果成功了,把原来的尾节点的下一个节点指向参数节点
                    return t;                 //返回队列中的最后一个节点,也就是原尾节点;  如果设置尾节点失败了,继续自旋,直到在末尾插入到队列成功为止
                }
            }
        }
}

  这里有点绕,画个图吧:

  初始状态的AQS中的CLH队列:

 

   第一个线程尝试去加锁入队列:

    第一步,初始化一个空的头节点(CAS操作,防止别的线程也在初始化操作):

 

 

   第二步,将头结点指向尾节点:

 

  第三步,开始自旋,防止此时有别的线程想要入队列,代码进入else里面,如果入队cas失败,一直自旋下去,直到插入到尾节点为止,入队成功enq方法会返回这个去尝试入队的节点:

 

 

   同理,第二个线程入队列:

  

 

  我们继续往下看acquireQueued方法:

final boolean acquireQueued(final Node node, int arg) {  //入参为入队列以后返回的线程,上面两个方法返回,arg为1,lock方法过来一直是1
        boolean failed = true;
        try {
            boolean interrupted = false;      //是否中断标记
            for (;;) {      //自旋,直到线程获取到锁,要么出现异常状态
                final Node p = node.predecessor();    //入参节点如果是非头结点的第一个节点,那么返回的就是头节点(只有头结点的下一个节点才会被唤醒后面会讲到,如果不是的话,就会走下面的if)
                if (p == head && tryAcquire(arg)) {    //如果入参节点上一个节点是头节点,会再去尝试获取一次锁,
                    setHead(node);              //如果获取到了锁,设置参数节点为头结点,后续节点都往前挪;如果抢锁失败了(为啥会失败,因为有可能非公平加锁模式下,别的线程上来就拿到锁了)走下面的if判断
                    p.next = null; // help GC        //断开与尾节点的引用
                    failed = false;
                    return interrupted;          //返回false
                }
                if (shouldParkAfterFailedAcquire(p, node) &&    
                    parkAndCheckInterrupt())      //如果前驱节点不是头结点,或者说是头结点,获取锁失败了,会走到这里。先去判断前驱节点的等待状态是不是-1
                    interrupted = true;          //(接上面)如果是-1就阻塞队列,等待唤醒,如果不是-1,就把前驱节点的等待状态改为-1,在把线程阻塞
} } finally { if (failed) cancelAcquire(node); } }  //异常处理

//入队列等待唤醒
/////

  都会先去更改前驱节点的waitStatus = -1,为了后面unpark操作,细节!细节!细节。unpark会将waitStatus
在改回0(为了支持非公平加锁外部新来线程拿到了锁,一直在-1和0之间反复横跳),就是为了acquireQueued方法中阻塞在parkAndCheckInterrupt里面的线程唤醒继续去抢锁。
整个CLH队列中,只有非head节点的第一个节点才会被唤醒,直到获取锁以后,唤醒下一个节点,成为新的head节点,然后出列。
///

private
static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus;      //前驱节点的等待状态 if (ws == Node.SIGNAL)    //如果是-1,表示下一个节点可以被唤醒,直接返回true /* * This node has already set status asking a release * to signal it, so it can safely park. */ return true; if (ws > 0) {      //如果前驱节点的等待状态比0大,那就标识当前线程异常了,把当前线程踢出队列 /* * Predecessor was cancelled. Skip over predecessors and * indicate retry. */ do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { /* * waitStatus must be 0 or PROPAGATE. Indicate that we * need a signal, but don't park yet. Caller will need to * retry to make sure it cannot acquire before parking. */ compareAndSetWaitStatus(pred, ws, Node.SIGNAL);  //如果不是-1,就把前驱节点的等待状态改为-1 } return false; }
private final boolean parkAndCheckInterrupt() {    //park住线程,等待去唤醒
        LockSupport.park(this);
        return Thread.interrupted();
    }

 

  6.ReentrantLock的公平与非公平加锁

  先看ReentrantLock的构造函数:

 

 

   内部类sync,aqs的实现,默认是非公平加锁,我们再来看NonfairSync这个实现类:

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

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();    //获取当前队列线程持有状态,如果是0,那就表示还未有线程占有锁
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {    //把state设置为1
                    setExclusiveOwnerThread(current);      //如果state成功设置成1,这个线程获取了锁,把当前线程设置为持有锁的线程
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {    //如果持有锁的已经是当前线程了,这个时候就是锁的重入
                int nextc = c + acquires;              //state+1
                if (nextc < 0) // overflow      //小于1,异常状态
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);      //设置state为新的值,1
                return true;
            }
            return false;
        }

  我们继续来看公平加锁实现,FairSync:

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)) {   //如果state为0,并且队列中没有别的在等待锁的线程,然后去更改state,把获取锁的线程设置为当前线程
                    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;
        }
    }

 

  7.独占模式加锁

  这个不过多解释了,aqs的属性exclusiveOwnerThread,标志为当前获取锁的线程是哪个,解锁以后,引用会置为空,重新等待新的线程去获取锁,再把引用指向为新的线程。

 

  8.手动解锁

  

public void unlock() {
        sync.release(1);
    }

  调用的是aqs的release方法:

public final boolean release(int arg) {
        if (tryRelease(arg)) {      //尝试去解锁
            Node h = head;      //如果成功了,获取头结点
            if (h != null && h.waitStatus != 0)    //如果头结点不为空,并且头节点的等待状态不为0(上面自旋的时候会把头结点的等待状态改为-1),就是为了这里去手动释放锁
                unparkSuccessor(h);    //解锁
            return true;
        }
        return false;
    }
protected final boolean tryRelease(int releases) {      //入参为1
            int c = getState() - releases;      //获取aqs中的state,在-1
            if (Thread.currentThread() != getExclusiveOwnerThread())    //如果获取锁的线程不是当前线程, 抛出异常
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {      //如果-1以后为0,表示释放锁成功
                free = true;
                setExclusiveOwnerThread(null);    //把当前获取锁的属性指向为空
            }
            setState(c);      //设置state为0    
            return free;
        }
//整个方法来说,如果线程是多次重入过,就需要多次解锁,最终让state为0才能释放锁
private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;      //获取头结点的等待状态
        if (ws < 0)        //如果是-1,将等待状态改为0
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        Node s = node.next;      //获取第一个有效节点
        if (s == null || s.waitStatus > 0) {    //为空,或者等待状态>0,说明出现异常,移除该异常节点出队列
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)    //不为空,将非头结点的第一个有效节点线程唤醒
            LockSupport.unpark(s.thread);
    }

  over~

posted on 2021-01-13 21:59  光輝歲月  阅读(161)  评论(0编辑  收藏  举报