• Java提高——JUC锁03-公平锁(一)


    1、AQS——指AbstractQueuedSynchronizer类

        AQS是Java中管理“锁”的抽象类,锁的许多公共方法都是在这个类中实现的。AQS是独占锁(如ReentrantLock)和共享锁(如Semaphore)的公共父类。

    2、AQS锁的类别——分为“独占锁”和“共享锁”

        1)独占锁:锁在一个时点只能被一个线程占有。根据锁的获取机制,它又划分为“公平锁”和“非公平锁”。公平锁,是按照通过CLH等待队列按照先来先得的规则,公平的获取锁;非公平锁,当线程要获取锁的时候,它会无视CLH等待队列而直接获取锁。独占锁的典型例子是ReentrantLock,此外,ReentrantReadWriteLock.WriteLock也是独占锁。

        2)共享锁:能被多个线程同时拥有,能被共享的锁。JUC包中的ReentrantReadWriteLock.ReadLock、CyclicBarrier、CountDownLatch和Semaphre都是共享锁。

    3、CLH队列——Craig、Landin、and Hagersten lock queue

        CLH队列是AQS中“等待锁”的线程队列。在多个线程中,为了保护竞争资源不被多个线程同时操作出现错误,我们常常需要通过锁来保护这些资源。在独占锁中,竞争资源在一个时点只能被一个线程访问,而其他线程则需要等待。CLH就是管理这些这些“等待锁”的线程的队列。

        CLH是一个非阻塞的FIFO队列。也就是说往里面插入或移除一个节点的时候,在并发条件下不会阻塞,而是通过自旋锁和CAS保证节点插入和移除的原子性。

    4、CAS——Compare And Swap

        CAS函数,是一个比较并交换的函数,它是原子操作函数;即通过CAS操作的数据都是以原子的方式进行的。例如,compareAndSetHead( ),compareAndSetTail( ),compareAndSetNext( )等函数。它的共同特点是,这些函数所执行的动作是以原子的方式进行的。

    ReentrantLock数据结构

    ReentrantLock的UML图


    可以看出:

    1)ReentrantLock实现了Lock接口

    2)ReentrantLock与sync是组合关系。ReentrantLock中包含了Sync对象;而且Sync是AQS的子类;更重要的是Sync有两个子类,公平锁(FairSync)和非公平锁(NonfairSync)。ReentrantLock是一个独占锁,至于是公平的还是非公平取决于sync对象是FareSync实例还是NonfairSync实例。


    获取公平锁

    1、Lock

    lock( )在ReentrantLock中的FairSync类中实现,源码:

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

    当前线程实际上是通过acquire(1)获取锁的。

    这里的“1”是设置锁状态的参数,对于独占锁,锁处于可获取的状态时,它的状态值是0;锁被线程初次获取到了,状态值就是1。

    由于ReentrantLock(公平锁/非公平锁)是可重入锁,所以“独占锁”可以被线程多次获取,每次获取就将锁的状态+1。初次获取锁时,通过acquire(1)将锁的状态设为1;再次获取的时候将锁的状态设为2,以此类推----这就是为什么获取锁时传入的参数为1的原因。

    可重入是指锁可以被单个线程多次获取。

    2、acquire( )

    acquire( )在AQS中实现的,源码:

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

    1)“当前线程”通过acquire( )获取锁,如果成功则直接返回,如果失败则进入到等待队列中排队等待(前面可能有线程在等待锁)

    2)“当前线程”获取失败的情况下,先通过addWaiter(Node.EXCLUSIVE)将当前线程加入到CLH队列(非阻塞的FIFO队列)末尾。CLH队列就是线程等待队列。

    3)执行完addWaiter之后,会调用acquireQueued( )来获取锁。由于此时ReentrantLock是公平锁,它会根据公平性原则来获取锁。

    4)“当前线程”在执行acquireQueued()时,会进入到CLH队列中休眠等待,直到获取锁了才返回!如果“当前线程”在休眠中被中断,acquireQueued()会返回true,此时,“当前线程”会调用selfInterrupt( )来自己给自己产生一个中断   

    接下来介绍上面源码中的各个方法

    一、tryAcquire( )

    1、公平锁的tryAcquire()在ReentrantLock的FairSync类中的实现,源码:

    protected final boolean tryAcquire(int acquires) {
            //获取当前线程
            final Thread current = Thread.currentThread();
            //获取独占锁的状态
            int c = getState();
            //若锁没有被任何线程拥有
            //则判断,当前线程是不是CLH中的第一个线程
            //若是,则获取该锁,设置锁的状态,并且设置锁的拥有者为当前线程
            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;
        }
    }
    根据源码得知tryAcquire()只是尝试获取锁,成功返回true,失败返回false,后续再通过其他办法获取锁。

    2、hasQueuedPredecessor()在AQS中的实现

    public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }
    hasQueuedPredecessor()是判断当前线程在CLH队列是不是队首,返回AQS中是不是比“当前线程”等待更久的线程。

    3、Node的源码

    //CLH队列的节点
    static final class Node {
        /** Marker to indicate a node is waiting in shared mode */
        static final Node SHARED = new Node();
        /** Marker to indicate a node is waiting in exclusive mode */
        static final Node EXCLUSIVE = null;
    
        /** 线程被取消后waitStatus的值  */
        static final int CANCELLED =  1;
        /** 当前线程的后续线程需要被唤醒时waitStatus的值
        一般发生的情况是:当前线程的后续线程处于阻塞状态,而当前线程被release或cancle掉,
        因此需要唤醒当前线程的后续线程
       */
        static final int SIGNAL    = -1;
        /** 线程(处在Condition休眠状态)在等待Condition唤醒,对应的waitStatus的值 */
        static final int CONDITION = -2;
        /**
         * 其他线程获取到“共享锁”对应的waitStatus的值
         * unconditionally propagate
         */
        static final int PROPAGATE = -3;
    
        /* * 
       *   waitStatus为:
        *  SINGNAL:
         *   CANCELLED:  
         *   CONDITION:  
         *   PROPAGATE: 时,分别表示不同的状态 
         *   若waitStatus为0:  则意味着当前线程不属于上面任何一种状态
         *
         */
        volatile int waitStatus;
    
        /**
         * 前一节点
         */
        volatile Node prev;
    
        /**
         * 后一节点
         */
        volatile Node next;
    
        /**
         * 节点所对应的线程
         */
        volatile Thread thread;
    
        /**
         * nextWaiter是区别当前锁为独占锁队列还是共享锁队列的标记
         * 若nextWaiter=SHARED.  则CLH是独占锁队列
         * 若nextWaiter=EXCLUSIVE(即nextWaiter=null),则CLH是共享锁队列
         */
        Node nextWaiter;
    
        /**
         * 共享锁则返回true,独占锁则返回false
         */
        final boolean isShared() {
            return nextWaiter == SHARED;
        }
    
        /**
         * 返回前一节点
         */
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }
    
        Node() {    // Used to establish initial head or SHARED marker
        }
       //构造函数,thread是节点所对应的线程,mode是用来表示thread是独占锁还是共享锁
        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }
        //构造函数,thread是节点所对应的线程,waitStatus是线程的等待状态
        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }
    

    Node是CLH队列的节点,代表等待锁的线程队列。

    1)每个Node都会有一个线程对应

    2)每个Node都会通过pre和next分别指向上一个节点和下一个节点,这分别代表上一个等待线程和下一个等待线程

    3)Node通过waitStatus保存线程的等待状态

    4)Node通过nextWaiter来区分是独占锁还是共享锁。如果是独占锁,则nextWaiter的值为EXCLUSIVE;如果是共享锁,则nextWaiter的值为SHARED。

    4、compareAndSetState( )

    compareAndSetState()在AQS中的实现,源码:

    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

    compareAndSetState()是sun.misc.Unsafe类中的一个本地方法。对此我们需要了解compareAndSetState()是以原子的方式操作当前线程;若当前线程的状态是expect,则它设置的状态是update。

    5、setExclusiveOwnerThread( )

    setExclusiveOwnerThread( )是在AbstractOwnableSynchronizer中实现,源码:

    /**
     * exclusiveOwnerThread是当前拥有独占锁的线程
     */
    private transient Thread exclusiveOwnerThread;
    
    /**
     * setExclusiveOwnerThread的作用就是设置线程为独占线程
     */
    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }

    6、getState()、setState()

    /**
     * 锁的状态
     */
    private volatile int state;
    
    /**
     * 获取锁的状态
     */
    protected final int getState() {
        return state;
    }
    
    /**
     * 设置锁的状态
     */
    protected final void setState(int newState) {
        state = newState;
    }
    

    state表示锁的状态,对于独占锁,state=0表示锁处于可获取状态(即锁没有被任何线程持有)。由于Java中的独占锁是可重入锁。state的值可以>1。

    小结:tryAcquire()的作用就是让当前线程尝试获取锁。获取成功返回true,失败返回false。

    二、addWaiter(Node.EXCLUSIVE)

    addW(Node.EXCLUSIVE)的作用就是创建“当前线程”的Node节点,且Node中记录“当前线程”对应的锁是独占锁类型,并将该节点添加到CLH队列的末尾。

    1、addWaiter()在AQS中实现的源码,源码:

    private Node addWaiter(Node mode) {
        //创建一个节点,节点对应当前线程,线程的锁的模型是mode
       Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        //若CLH不为空,则将当前线程添加到CLH末尾
       Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //若CLH为空,则调用enq方法新建CLH队列,然后再将当前线程添加到CLH队列中
       enq(node);
        return node;
    }

    2、compareAndSetTail( )

    compareAndSetTail( )在AQS中的实现,源码如下:

    private final boolean compareAndSetTail(Node expect, Node update) {
        return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
    }
    

    compareAndSetTail( )也属于CAS函数,通过本地方法实现的。compareAndSetTail(expect,update )会以原子的方式操作,他的作用是判断CLH的队尾是不是expect,是的话,就将队尾设置为update。

    3、enq( )

    enq( )在AQS中的实现,源码:

    private Node enq(final Node node) {
        for (;;) {
            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;
                }
            }
        }
    }

    enq的作用,如果CLH队列为空,则新建一个CLH表头;然后将node添加到CLH末尾。否则直接将node添加到CLH末尾。

    小结:addWaiter的作用是将当前线程添加到CLH队列中。这就意味着将当前线程添加到等待获取锁的等待线程队列中了。

    三、acquireQueued()

    1、acquireQueued()在AQS中的实现,源码:

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            //interrupted表示在CLH调度中,当前线程在休眠中有没有被中断过
           boolean interrupted = false;
            for (;;) {
                 // 获取上一个节点。node是当前线程对应的节点,这就意味着获取上一个等待锁的线程
               final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

    acquireQueued()的目的是从队列中获取锁。

    2、shouldParkAfterFailedAcquire()

    shouldParkAfterFailedAcquire()在AQS中的实现,源码如下:

    //返回当前线程是否应该阻塞
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //前继节点的状态
       int ws = pred.waitStatus;
        //如果前继节点是SIGNAL状态,则意味着当前线程需要被unpark唤醒。此时返回true
       if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {
            /*如果前继节点为取消状态,则设置当前节点的当前前继节点为原前继节点的前继节点。
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*如果前继节点为0或者共享状态,则设置前继节点为SIGNAL状态
             * 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);
        }
        return false;
    }
    

    1)关于waitStatus参考:

    CANCELLED[1] -- 当前线程已被取消

    SIGNAL[-1] -- “当前线程的后继线程需要被unpark(唤醒)”。一般发生情况是:当前线程的后继线程处于阻塞状态,而当前线程被release或cancel掉,因此需要唤醒当前线程的后继线程。

    CONDITION[-2] -- 当前线程(处在Condition休眠状态)在等待Condition唤醒

    PROPAGATE[-3] -- (共享锁)其它线程获取到“共享锁”[0] -- 当前线程不属于上面的任何一种状态。

    2)shouldParkAfterFailedAcquire()通过以下规则,判断当前线程是否需要被阻塞:

    ①如果前继节点状态为SINGNAL,表明当前节点需要被unpark(唤醒),此时则返回true

    ②如果前继节点状态我CANCELLED(ws>0),说明前继节点被取消,则通过先前回溯找到一个有效的节点,并返回false

    ③如果前继节点状态为非SINGNAL、非CANCELLED,则设置前继节点的状态为SIGNAL,并返回false

    如果“规则1”发生,即“前继节点是SIGNAL”状态,则意味着“当前线程”需要被阻塞。接下来会调用parkAndCheckInterrupt()阻塞当前线程,直到当前先被唤醒才从parkAndCheckInterrupt()中返回。

    3、parkAndCheckInterrupt()

    parkAndCheckInterrupt()在AQS中的实现,源码:

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);//通过LockSupport的park阻塞当前线程
        return Thread.interrupted();//返回线程的中断状态
    }

    parkAndCheckInterrupt()的作用是阻塞当前线程,并返回当前线程被唤醒后的中断状态。它首先会通过LockSupport.park()阻塞当前线程,然后通过Thread.interrupted返回线程的中断状态。

    阻塞之后如何唤醒:

    情况1:unpark()唤醒,前继节点对应的线程使用完锁之后,通过unpark方式唤醒线程

    情况2:中断唤醒。其他线程通过interrupt中断当前线程。

    补充:LockSupport中的park和unpark的作用和Object中的wait、notify作用类似,是阻塞和唤醒。它们用法不同,park和unpark是轻量级的,而wait和notify是必须先通过Synchronized获取同步锁。

    4、再次tryAcquire()

    final Node p = node.predecessor();
    if (p == head && tryAcquire(arg)) {
        setHead(node);
        p.next = null; // help GC
        failed = false;
        return interrupted;
    }
    1)通过node.predecessor( )获取前继节点。prodecessor( )就是返回node的前继节点

    2)p==head&&tryAcquire( args)

     首先判断前继节点是不是CLH表头,如果是则通过tryAcquire尝试获取锁。

    其实这么做的原因是为了让当前线程获取锁 。为什么要先判断p==head呢?因为这样做是为了保证公平性:

    a、前面我们在shouldParkAfterFailedAcquire()判断当前线程是否需要阻塞

    b、接着,,当前线程阻塞的话,会调用parkAndCheckInterrupt( )来阻塞线程。当前线程被解除阻塞的时候,我们会返回线程的中断状态。而线程被解决阻塞可能是由于“线程被中断” ,也可能其他线程调用该线程的unpark函数。

    c、再回到p==head这里,如果当前线程因为其他线程调用了unpark函数而被唤醒,那么唤醒它的线程应该是前继节点所对应的线程。

          再来理解p==head:当前继节点是CLH队列的头节点的时候,并且释放锁之后;就轮到当前节点获取锁了。然后当前节点通过tryAcquire()获取锁;获取成功则通过setHead(node)设置当前节点为头结点,并返回。

    总之,如果前继节点调用unpark函数唤醒了当前线程,并且前继节点是CLH的表头,此时满足p==head,也就符合公平性原则。否则,如果当前线程因为“线程中断”而被唤醒,那么就显得不公平。这就是为什么说p==hend是公平性原型的保证。

    小结:acquireQueued()的作用就是“当前线程”会根据公平性原则进行阻塞等待,直到获取锁为止;并返回当前线程在等待过程中有没有 被中断过。

    四、selfInterrupt( )

    selfInterrupt()在AQS中的源码如下:

    static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }
    

    意思就是 “当前线程”自己产生一个中断。原因是什么拉?

    必须结合acquireQueued( )进行说明。如果在acquireQueue( )中,当前线程被中断过,则执行selInterrupt();否则不执行。

    在acquireQueue()中,即使线程在阻塞状态被中断唤醒而获取到CPU执行权利;但是,如果该线程的前面还有其他等待锁的线程,根据公平性原则该线程依然无法获取到锁。它会在次阻塞,直到该线程被它前面的等待锁的线程唤醒;线程才会获取锁,然后“真正执行起来”!

    也就是说,在该线程“成功获取锁真正执行起来”之前,它的中断会被忽略并且中断标记会被清除!因为在parkAndCheckInterrupt ()中,我们的线程中断状态调用了Thread.interrupted()。该函数不同于Thread的isInterrupted( )函数,isInterrupted()仅仅返回中断状态,而interrupted( )在返回当前中断状态之后,还会清除中断状态。正因为之前的中断状态被清除了,所以这里需要调用selInterrupted( )重新产生一个新的中断。

    小结:selInterrupted()的作用就是当前线程自己产生一个中断。

    总结:

    再回头来看,acquire( )函数的最终目的是获取锁!

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

    1)、先通过tryAcquire( )尝试获取锁。获取成功的话,直接返回;获取失败的话则通过acquireQueued()获取锁

    2)、尝试失败的情况下会先通过addWaiter()来将当前线程“加入到CLH队列”末尾;然后调用acquireQueued(),在CLH队列中排队等待获取锁,在此过程中线程处于休眠状态。直到获取锁才会返回。如果在休眠等待过程中被中断过,则调用isInterrupt()来自己产生一个中断。

    转载请注明出处:http://www.cnblogs.com/skywang12345/p/3496147.html

  • 相关阅读:
    git学习笔记
    ubuntu常用命令
    hdfs[命令] fsck
    hdfs[命令] dfsadmin
    hdfs[命令] dfs
    Hadoop2.0新特性-持续追加【干货】
    Cloudera 建议使用 NTP 使 Hadoop 群集实现时间同步
    Cloudera CDH5 部署实战指南(离线安装)
    没有用户画像,别谈精准营销
    用户画像数据建模方法
  • 原文地址:https://www.cnblogs.com/huangzhe1515023110/p/9276039.html
Copyright © 2020-2023  润新知