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

猜你喜欢

转载自blog.csdn.net/qq_30604989/article/details/80755217
今日推荐