Android JUC — AQS+锁

JUC:java.util.concurrent

1.AQS

AQS全称AbstractQueuedSynchronizer,即抽象队列同步器,是阻塞式锁和同步器工具的框架。

其实AQS的本质就是JUC并发包下的一个基类,ReentrantLock、Semaphore、CyclicBarrier、CountDownLatch等并发类都是基于AQS实现的。具体做法是继承AQS并实现其模板方法,从而达到同步状态的管理。

AQS的核心思想是:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效工作线程,并将共享资源设置为锁定状态;如果被请求的共享资源被占用,就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制在AQS里是用CLH队列实现的。AQS内部维护了一个等待队列(CLH队列)进行排队,将暂时获取不到锁的线程加入到队列中,并用一个volatile类型的变量state表示锁状态,然后子类用CAS的方式去操作。

AQS可以实现独占锁和共享锁。ReentrantLock是独占锁。ReentrantReadWriteLock是独占锁和共享锁。CountDownLatch是共享锁。

 

2.AQS源码分析

①AQS结构

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {

    protected AbstractQueuedSynchronizer() { }

    private transient volatile Node head;//头结点

    private transient volatile Node tail;//尾结点

    private volatile int state; //同步状态(state为0表示无锁; state>0时有锁)

    protected final int getState() { //获取锁状态

        return state;

    }

    protected final void setState(int newState) {

        state = newState; //设置锁状态

    }

}

AQS内部包含了一个队列和一个volatile int类型的state变量。

head、tail和state都是volatile类型变量,volatile可以保证多线程的内存可见性。

同步队列的基本结构:

d9257791964d46928a5171122ace706c.png

同步队列的Node类源码解析:

static final class Node {

    static final Node SHARED = new Node();//共享模式节点

    static final Node EXCLUSIVE = null;//独占模式节点

    static final int CANCELLED = 1;//取消状态

    static final int SIGNAL = -1; //通知状态,说明后继节点需要被唤醒

    static final int CONDITION = -2;//条件等待状态

    static final int PROPAGATE = -3;//传播状态

    volatile int waitStatus;//等待状态标志

    volatile Node prev;//前驱节点

    volatile Node next;//后继节点

    volatile Thread thread;//节点对应的线程

    Node nextWaiter;//等待队列中的后继节点

    final boolean isShared() { //当前节点是否处于共享模式等待

        return nextWaiter == SHARED;

    }

    final Node predecessor() { //获取前驱节点,如果为空则抛出空指针异常

        Node p = prev;

        if (p == null) {

            throw new NullPointerException();

        } else {

            return p;

        }

    }

    Node() { }

    Node(Thread thread, Node node) { //addWaiter会调用此构造函数

        this.nextWaiter = node;

        this.thread = thread;

    }

    Node(Thread thread, int waitStatus) { //Condition会用到此构造函数

        this.waitStatus = waitStatus;

        this.thread = thread;

    }

}

Node节点是对每一个访问同步代码的线程的封装,包含了需要同步的线程本身以及线程的状态,如是否被阻塞,是否等待唤醒,是否已经取消等。每个Node节点内部关联其前驱节点prev和后继节点next,这样方便线程释放锁后快速唤醒下一个在等待的线程。

SHARED和EXCLUSIVE常量分别代表共享模式和独占模式。共享模式是一个锁允许多条线程同时操作,如信号量Semaphore采用的就是基于AQS的共享模式实现的。独占模式是同一个时间段只能有一个线程对共享资源进行操作,多余的请求线程需要排队等待,如ReentrantLock。

从Node结构中prev和next节点可知,node是一个双向链表,waitStatus存储了当前线程的状态信息:

CANCELLED (1) :当前线程因为超时或者中断被取消。这是一个终结态,也就是状态到此为止。

SIGNAL (-1) :当前线程的后继线程被阻塞或即将被阻塞,当前线程释放锁或者取消后需要唤醒后继线程。这个状态一般都是后继线程来设置前驱节点的。

CONDITION (-2):当前线程在condition队列中。

PROPAGATE (-3) :用于将唤醒后继线程传递下去,这个状态的引入是为了完善和增强共享锁的唤醒机制。在一个节点成为头节点之前,是不会跃迁为此状态的。

0 表示无状态。

AQS使用了模板方法模式。自定义同步器时需要继承AbstractQueuedSynchronizer并重写下面几个AQS提供的钩子方法:

boolean tryAcquire(int arg) 独占方式,尝试获取独占锁。成功返回true,失败返回false。

boolean tryRelease(int arg) 独占方式,尝试释放独占锁。成功返回true,失败返回false。

int tryAcquireShared(int arg) 共享方式,尝试获取共享锁。负数表示失败;0表示成功,但没有剩余可用锁;正数表示成功,且有剩余锁。

boolean tryReleaseShared(int arg) 共享方式,尝试释放共享锁。成功返回true,失败返回false。

boolean isHeldExclusively() 当前线程是否正在独占锁。只有用到condition才需要实现它。

②acquire()-独占模式请求获取锁

public final void acquire(int arg) {

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

        selfInterrupt();

}

1)首先调用tryAcquire()方法尝试获取共享资源,如果成功则返回true。

tryAcquire()是模板方法,由子类实现具体操作。

2)如果tryAcquire()失败,则调用addWaiter()方法把当前线程封装成一个独占模式的Node节点,并使用CAS操作把该节点追加到等待队列的尾部。

addWaiter()方法作用是:判断等待队列是否为空,如果是空则初始化之后再将自己插入,否则直接将自己插入队尾。

private Node addWaiter(Node mode) {

    Node node = new Node(mode);//将当前线程封装成一个独占模式的节点

     for (;;) { //自旋,只有compareAndSetTail( oldTail, node)返回true,即当前线程成功插入队尾以后才会return结束for循环,否则就一直在这里自旋尝试插入队尾

         Node oldTail = tail;

         if (oldTail != null) {

             node.setPrevRelaxed(oldTail); //将当前线程节点的前驱节点设置为刚才的尾节点

             if (compareAndSetTail(oldTail, node)) { //用CAS方式将当前线程节点设置为队列的尾节点

                 oldTail.next = node; //刚才尾节点的后继节点设置为当前线程节点

                 return node; //将当前线程节点返回

             }

         } else { //尾节点为空,说明队列为空,需要先初始化队列

             initializeSyncQueue();

         }

     }

 }

Node node = new Node(mode);表示用当前线程创建一个EXCLUSIVE的Node:

Node(Node nextWaiter) {

    this.nextWaiter = nextWaiter;

    THREAD.set(this, Thread.currentThread());

}

当AQS中队列为空时会调用initializeSyncQueue()方法初始化头节点和尾节点,然后再将当前线程节点放到队列尾部:

private final void initializeSyncQueue(){

    Node h;

    if(HEAD,.compareAndSet(this, null, (h=new Node()))) //初始化一个Node作为头节点,这个头节点没有任何意义(没有thread),不属于排队线程

        tail = h;

}

addWaiter()方法执行完成后,会返回当前线程的节点,然后执行acquireQueued()方法(注意在执行该方法之前已经将当前线程节点加入AQS的队列中)。

3)当前线程的节点加入队列尾部后,该节点的线程进入自旋状态。自旋时,判断自旋节点的前驱节点是不是头节点,如果前驱节点是头节点,则该节点再次尝试获取,如果仍未获取到,则把自旋节点的线程挂起,等待前驱节点唤醒;如果此时尝试获取成功,则将该节点设置为头节点。

acquireQueued()方法的作用:判断当前线程结点的前驱结点是否为头结点,如果前驱节点是头结点就尝试再次获取锁,否则就直接调用park()睡眠。如果不能获取锁就一直睡眠停留在这里,否则就会返回执行用户编写的代码。

final boolean acquireQueued(final Node node, int arg) {

     try {

        boolean interrupted = false;

         for (;;) { //自旋开始,如果前驱节点是head即当前节点是排队的第一个,这时候如果当前节点成功获取锁就return结束for循环,否则需要确保前驱节点的状态为SIGNAL然后将当前节点park。如果获取锁失败或者修改前驱节点为SIGNAL时失败,就会继续下一次循环再次尝试

             final Node p = node.predecessor();//获取当前节点的前驱节点

             if (p == head && tryAcquire(arg)) { //如前驱节点是head则再次尝试加锁,如果此时加锁成功就将当前节点设置为新的头节点

                 setHead(node); 

                 p.next = null; // 将之前的头节点置为null,便于GC回收

                 return interrupted;//整个方法里只有这里有return,也就是只有这里会结束该方法

             }

            //前驱节点不是头节点,或者前驱节点是头节点但是再次尝试获取锁失败,此时需要将当前线程挂起,但是挂起时要保证该节点的前驱节点是有效节点,而且要保证前驱节点的状态是SIGNAL(只有前一个节点的状态是SIGNAL才会唤醒下一个节点)。shouldParkAfterFailedAcquire方法返回true代表前驱节点有效,此时调用parkAndCheckInterrupt方法将当前线程挂起(上一个节点的等待状态是-1时,shouldParkAfterFailedAcquire才会返回true,返回true后调用parkAndCheckInterrupt方法将线程阻塞,等待唤醒获取锁)

             if (shouldParkAfterFailedAcquire(p, node))

                interrupted |= parkAndCheckInterrupt();

         }

     } catch(Throwable t) {

         cancelAcquire(node); 

        throw t;

     }

 }

看一下setHead()方法:

private void setHead(Node node) {

    head = node;

    node.thread = null; //头节点不需要thread属性,队列中的一个节点获取了锁以后,就不需要再排队了,因此将它的thread置为null,并将它设置为新的头节点

    node.prev = null;

}

节点获取到锁资源后无需再排队,此时将该节点变成头节点,因为头节点就是没有意义的,即不需要排队。但是队列中该节点不删掉,作为头节点使用。

再看shouldParkAfterFailedAcquire()方法:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {

    int ws = pred.waitStatus;

    if(ws == Node.SIGNAL) //如果前驱节点的等待状态为SIGNAL,那么以后可以正常唤醒该线程,那就没问题

        return true;//前驱节点的等待状态是-1才会返回true

    if(ws > 0) { //前驱节点为CANCELLED状态,即已经失效。需要将CANCELLED节点舍弃点,找到它之前的有效节点作为前驱节点

        do {

            node.prev = pred = pred.prev;//将当前节点的prev指针指向前驱节点的前驱节点

        } while (pred.waitStatus > 0); //找到队列中排在当前节点之前并且等待状态不为CANCELLED的节点

        pred.next = node;//将找到的有效的前一个节点的next指向当前节点

    } else { //等待状态不是SIGNAL或CANCELLED此时将前一个有效节点的状态改为SIGNAL

        pred.compareAndSetWaitStatus(ws, Node.SIGNAL);

    }

    return false;

}

最后是parkAndCheckInterrupt()方法:

private final boolean parkAndCheckInterrupt(){

    LockSupport.park();

    return Thread.interrupted();

}

将当前线程挂起。

当acquireQueued()方法出现代码异常时,会调用cancelAcquire()方法:

private void cancelAcquire(Node node) { //参数node为当前节点

    if(node == null) return;

    node.thread = null; //将当前节点的线程置为空,即竞争锁资源跟我没关系了

    Node pred = node.pred;

    //如果当前节点的前驱节点为CANCELLED状态,则跳过该前驱节点。通过while循环找到当前节点之前的最近的有效节点

    while(pred.waitStatus > 0) 

        node.prev = pred = pred.prev;

    //将找到的最近的有效节点声明为predNext

    Node predNext = pred.next;

    node.waitStatus = Node.CANCELLED;//将当前节点置为失效节点

    if(node == tail && compareAndSetTail(node, pred)) { //如果当前节点为尾节点,(由于当前节点为失效节点)则将刚刚找到的离当前节点最近的有效节点置为尾节点(这样就把当前节点以及它之前所有失效的节点都跳过了)

        pred.compareAndSetNext(predNext, null);//用CAS的方式将尾节点的next置为null(这样就与失效节点都断开了链接)

    } else {

        int ws;

        if(pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && pred.compareAndSetWaitStatus(ws, Node.SIGNAL))) && pred.Thread != null) { //当前节点的前驱节点不是头节点,并且前驱节点有效,此时需要把前驱节点的next到指向自己的next(自己已经为CANCELLED状态,需要跳过自己)

            Node next = node.next; //自己的next节点

            if(next != null && next.waitStatus <=0)

                pred.compareAndSetNext(predNext, next);//用CAS方式把当前节点的next赋值给前驱节点的next(跳过自己)

        } else { //如果当前节点是头节点,而当前节点已经是CANCELLED状态,此时需要唤醒它的后继节点

            unparkSuccessor(node);//唤醒当前节点的后继节点

        }

        node.next = node;//help GC

    }

}

③release()-独占模式请求释放锁

public final boolean release(int arg) {

    if(tryRelease(arg)) { //模板方法

        Node h = head; //头节点

        if(h != null && h.waitStatus != 0) //如果队列非空且头结点的状态标识为SIGNAL

            unparkSuccessor(h); //唤醒头结点的后继结点,即排队的第一个节点

        return true; //释放锁成功

    }

    return false; //释放锁失败(该锁被重入了,释放次数<加锁次数)

}

1)调用tryRelease()释放共享资源,即state=0。

tryRelease()是模板方法。

2)释放共享资源后唤醒没有被中断的后驱节点的线程。

主要是unparkSuccessor()方法:

private void unparkSuccessor(Node node) {

    int ws = node.waitStatus; //获取等待状态,此时可能为SIGNAL或CANCELLED。如果为CANCELLED意味着node已经被唤醒但发现自己被中断需要继续往后唤醒新的结点

    //如果当前结点状态标识为SIGNAL

    if (ws < 0) //不是CANCELLED状态

        node.compareAndSetWaitStatus(ws, 0); //使用CAS方式修改当前结点状态标识为0,即初始状态

    Node s = node.next; //获取后继结点

    if (s == null || s.waitStatus > 0) { //如果队列中没有等待的线程或当前节点的后继节点的等待状态为CANCELLED,则从后往前找到最后一个(也就是从前往后第一个)等待状态不是CANCELLED的结点,且不能是当前结点

        s = null;

        for (Node t = tail; t != null && t != node; t = t.prev)

            if (t.waitStatus <= 0)

                s = t;

    }

    if (s != null) //如果找到了最靠前的等待状态为SIGNAL的节点,即队列中有等待被唤醒的线程,就调用unpark方法唤醒该节点的线程

        LockSupport.unpark(s.thread);  //唤醒第一个等待的线程(被阻塞的线程会在parkAndCheckInterrupt()中继续向下执行)

}

④acquireShared()-共享模式请求获取锁

public final void acquireShared(int arg) {

    if(tryAcquireShared(arg) <0) 

        doAcquireShared(arg);

}

tryAcquireShared(arg)是模板方法,需要子类具体实现,其返回值小于0说明获取共享锁失败(相当于独占锁tryAcquire返回false),然后调用doAcquireShared()将当前线程加入等待队列。

private void doAcquireShared(int arg) {

    final Node node=addWaiter(Node.SHARED);

    boolean interrupted = false;

    try {

        for(;;) {

            final Node p = node.predecessor();

            if(p == head) {

                int r = tryAcquireShared(arg);

                if(r > 0) {

                    setHeadAndPropagate(node,r);

                    p.next = null;

                    return;

                }

            }

           if(shouldParkAfterFailedAcquire(p,node)

                interrupted |= parkAndCheckInterrupt();

        }

    } catch (Throwable t) {

        cancelAcquire(node);

        throw t;

    } finally {

        if(interrupted)

            selfInterrupt();

    }

}

和独占锁的acquire()方法很类似,区别是Node节点为SHARED,获取锁方法tryAcquireShared小于0时表示无法获取锁。

⑤releaseShared()-释放共享锁

public final boolean releaseShared(int arg) {

    if(tryReleaseShared(arg)) {

        doReleaseShared();

        return true;

    }

    return false;

}

tryReleaseShared()是模板方法,由子类具体实现,其返回值为true表示释放成功。

private void doReleaseShared(){

    for(;;) {

        Node h = head;

        if(h != null && h != tail) {

            int ws = h.waitStatus;

            if(ws == Node.SIGNAL) {

                if(!h.compareAndSetWaitStatus( Node.SIGNAL, 0))

                    continue;

                unparkSuccessor(h);

            } else if(ws == 0 && !h.compareAndSe tWaitStatus(0, Node.PROPAGATE))

                continue;

        }

        if(h == head)   break;

    }

}

 

3.synchronized同步锁

synchronized是同步锁,也称互斥锁、内部锁,用于多线程同步。

当多个线程同时访问被synchronized修饰的方法时,同一时间有且仅有一个线程可以访问该方法,当一个线程在访问时,其它线程只能等待,这个线程访问完毕后,下一个线程才可以访问。

每个对象只有一个锁,谁能够拿到这个锁谁就有访问权限。在多线程运行过程中,线程会去抢对象的监视器monitor,这个监视器是对象独有的(其实就相当于一把钥匙),抢到了就能获得当前代码块的执行权,其他没有抢到的线程就会进入队列SynchronizedQueue中等待,等待当前线程执行完后释放锁。当前线程执行完毕后会通知出队,然后继续重复当前过程。从jvm的角度来看monitorenter和monitorexit指令代表着代码的执行与结束 。

synchronized锁的特点:
①监视器锁:使用synchronized实现的线程同步是通过监视器monitor实现的,所以内部锁也叫监视器锁。
②自动获取/释放:线程对同步代码块的锁的申请和释放由JVM内部实施,线程在进入同步代码块前会自动获取锁,并在退出同步代码块时自动释放锁,这也是同步代码块被称为内部锁的原因。(异常时也会自动释放)
③锁定方法/类/对象:synchronized关键字可以用来修饰方法,锁住特定类和特定对象。
④临界区:同步代码块就是内部锁的临界区,线程在执行临界区代码前必须持有该临界区的内部锁。
⑤锁句柄:内部锁锁的对象就叫锁句柄(就是刚才代码里面的this或者lock对象),锁句柄通常会用 private和final关键字进行修饰。因为锁句柄变量一旦改变,会导致执行同一个同步代码块的多个线程实际上用的是不同的锁。
⑥不会泄漏:内部锁不会导致锁泄漏,因为javac 编译器把同步代码块编译为字节码时,对临界区中可能抛出的异常做了特殊处理,这样临界区的代码出了异常也不会妨碍锁的释放。
⑦非公平锁:内部锁是使用的是非公平策略,是非公平锁,也就是不会增加上下文切换开销。

synchronized分为类锁和对象锁,即可以锁类也可以锁对象。在多线程访问时,这两个锁有很大的区别,对象锁是用于对象实例方法或者一个对象实例上的;类锁是用于类的静态方法或者一个类的class对象上的。类的对象实例可以有很多个,但是每个类只有一个Class对象。注意:不同对象实例的对象锁是互不干扰的;每个类只有一个类锁;类锁和对象锁互相不干扰。

类锁的作用是防止多个线程同时访问添加了synchronized锁的代码块,而对象锁是防止其他线程访问同一个对象中synchronized代码块或者函数。

①对象锁

对象锁创建有两种方法:

//同步方法,对象锁

public synchronized void syncMethod() {

    ……

 }

//同步代码块,this对象锁

public void syncThis() {

    synchronized (this) {

       ……

    }

}

②类锁

类锁创建也有两种方法:

//同步class对象,类锁

public void syncClassMethod() {

    synchronized (SynchronizedDemo.class) {

        ……

    }

}

//同步静态方法,类锁

public static synchronized void syncStaticMethod(){

    ……

}

③类锁和对象锁比较

根据类锁和对象锁的概念,通过例子验证它们的区别。

首先有三个方法:

public class SynchronizedDemo {

    private int ticket = 10;

    //同步方法,对象锁

    public synchronized void syncMethod() {

        for (int i = 0; i < 1000; i++) {

            ticket--;

            System.out.println( Thread.currentThread().getName() + "剩余的票数:" + ticket);

        }

    }

    //同步块,对象锁

    public void syncThis() {

        synchronized (this) {

            for (int i = 0; i < 1000; i++) {

                ticket--;

                System.out.println( Thread.currentThread().getName() + "剩余的票数:" + ticket);

            }

        }

    }

    //同步Class对象,类锁

    public void syncClassMethod() {

        synchronized (SynchronizedDemo.class) {

            for (int i = 0; i < 50; i++) {

                ticket--;

                System.out.println( Thread.currentThread().getName() + "剩余的票数:" + ticket);

            }

        }

    }

}

(1)验证一:同一个对象实例,使用两个线程调用不同的对象锁方法(可实现同步)

final SynchronizedDemo synchronizedDemo = new SynchronizedDemo();

//线程一

new Thread() {

    @Override

    public void run() {

        synchronizedDemo.syncMethod();

    }

}.start();

//线程二

new Thread() {

    @Override

    public void run() {

        synchronizedDemo.syncThis();

    }

}.start();

由于使用的是同一个对象的对象锁,所以执行出来的结果是同步的(即先运行线程一,等线程一运行完后运行线程二,ticket有序的减少)。

(2)验证二:不同的对象实例,使用两个线程调用同一个对象锁方法(不可实现同步)

final SynchronizedDemo synchronizedDemo1 = new SynchronizedDemo();

final SynchronizedDemo synchronizedDemo2 = new SynchronizedDemo();

//线程一

new Thread() {

    @Override

    public void run() {

        synchronizedDemo1.syncMethod();

    }

}.start();

//线程二

new Thread() {

    @Override

    public void run() {

        synchronizedDemo2.syncMethod();

    }

}.start();

由于是两个不同的对象,它们的对象锁就是不同的,其结果是两个线程互相抢占资源的运行,即ticket偶尔会无序的减少,不能实现同步。

(3)验证三:同一个对象,使用两个线程,一个线程调用对象锁方法、一个线程调用类锁方法(不可实现同步)

final SynchronizedDemo synchronizedDemo = new SynchronizedDemo();

//线程一

new Thread() {

    @Override

    public void run() {

        synchronizedDemo.syncMethod();

    }

}.start();

//线程二

new Thread() {

    @Override

    public void run() {

        synchronizedDemo.syncClassMethod();

    }

}.start();

由于对象锁和类锁互不干扰,所以也是线程不安全的,不能实现同步。

因此得出结论:1、不同对象实例的对象锁互不干扰;2、每个类只有一个类锁。3、类锁和对象锁互相不干扰。

 

synchronized使用注意:

①synchronized锁定的是对象,而不是代码。

②synchronized(Object)中Object不能用String常量或Integer、Long等基本数据类型。

③作为锁的Object最好是final修饰,防止该Object发生变化导致锁失效。

④synchronized具有可重入性,是可重入性锁。比如一个类有两个synchronized方法,在m1方法中需要调用m2方法,如果synchronized不具有可重入性,那这里就没办法调用m2了。

⑤程序在执行过程中,如果出现异常,默认情况下锁会被释放。所以在开发过程中,有异常要多加小心,不然可能会发生不同步的情况。比如在一个app中,多个线程共同访问同一个资源,这时如果异常处理不合适,在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生的数据。因此要非常小心的处理同步业务逻辑中的异常。

ReentrantLock和synchronized的区别:

①ReentrantLock的加锁和解锁操作都需要手动完成。
②当异常发生时synchronized会自动释放锁,而ReentrantLock不会自动释放锁。因此需要将unlock()操作放在finally代码块中,保证任何时候锁都能够被正常释放掉。
③默认情况下synchronized和ReentrantLock都是非公平锁。但是ReentrantLock可以通过传入true来创建一个公平锁。

 

4.ReentrantLock可重入锁(同步锁、互斥锁)

在jdk1.6之前,synchronized被称为重量级锁。为了提高性能,出现了ReentrantLock锁。后来改进了synchronized,使得synchronized的执行效率和ReentrantLock差不多,甚至更好,但是由于ReentrantLock可以直接代码操作加锁/解锁、可中断获取锁等特性,因此使用的比较多。

ReentrantLock是java实现的公平锁/非公平锁,也是可重入锁,它是基于AQS实现的。

1)首先看一下ReentrantLock用法:

class X {

    private final ReentrantLock lock = new ReentrantLock(true);

    public void xx(){

        lock.lock();

        try{

            //do something

        } finally{

            lock.unlock();

        }

    }

}

ReentrantLock有一个抽象内部类Sync继承了AbstractQueuedSynchronizer,NonfairSync类和FairSync类均继承自Sync。ReentrantLock的默认构造函数中实例化了NonfairSync类和FairSync类。对于外部而言,只关注lock与unlock方法,但实际上内部调用了AbstractQueuedSynchronizer的方法。

2)下面看源码实现

(1)构造方法

public ReentrantLock() {

    sync = new NonfairSync(); //默认为非公平锁

}

public ReentrantLock(boolean fair) {

    sync = fair? new FairSync(): new NonfairSync();

}

(2)ReentrantLock加锁

ReentrantLock加锁会调用lock()方法:

public void lock() {

    sync.acquire(1); //调用AQS的acquire()方法

}

AQS的acquire()方法:

public final void acquire(int arg) {

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

        selfInterrupt();

}

其中tryAcquire()是模板方法,在ReentrantLock中会根据公平锁还是非公平锁有不同的实现。

①ReentrantLock中公平锁的tryAcquire()方法

static final class FairSync extends Sync {

  protected final boolean tryAcquire(int acquires){

         final Thread current = Thread.currentThread();

         int c = getState();

         if (c == 0) { //锁空闲

             //判断自己是否需要排队

             if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { //不需要排队,并且用CAS获取到锁

                 setExclusiveOwnerThread(current); //设置自己为持有锁的线程

                 return true;//加锁成功

             }

         } else if (current == getExclusiveOwnerThread()) { //锁不空闲,并且自己持锁,则锁重入

             int nextc = c + acquires;

             if (nextc < 0) //正整数的最大值加1会变成负数1(最高位的符号位变了)

                 throw new Error("Maximum lock count exceeded");

             setState(nextc);

             return true;//锁重入成功

         }

         return false;//加锁失败

     }

}

公平锁的tryAcquire()方法首先判断锁是否空闲,如果锁空闲则用hasQueuePredecessors()判断自己是否需要排队,如果不需要排队就用CAS操作尝试获取锁,获取锁成功则将持有锁的线程设置为自己。如果锁非空闲,则看是否为自己持有锁,若为自己持有则重入,否则返回false表示尝试获取锁失败,并将自己加入到等待队列。

//判断自己是否需要排队

public final boolean hasQueuedPredecessors(){

    Node h, s;

    if ((h = head) != null) { //head不为空

         if ((s = h.next) == null || s.waitStatus > 0) { //头节点的后继节点线程已取消

             s = null; 

             for (Node p = tail; p != h && p != null; p = p.prev) { //从后往前获取未取消的节点线程,并将最靠前的未取消的节点赋值给s

                 if (p.waitStatus <= 0) //等待状态是未取消的

                     s = p;

             }

         }

         if (s != null && s.thread != Thread.currentThread())  //队列中有等待着的线程且不是自己

             return true; //说明需要排队

     }

     //如果head为null,则不需要等待,因为没有等待队列

     return false;

 }

②ReentrantLock中非公平锁的tryAcquire()方法

static final class NonfairSync extends Sync {

    protected final boolean tryAcquire(int acquires){

        return nonfairTryAcquire(acquires);

    }

}

nonfairTryAcquire()方法的实现在ReentrantLock的Sync内部类里:

abstract static class Sync extends AbstractQueuedSynchronizer {

   final boolean nonfairTryAcquire(int acquires){

       final Thread current = Thread.currentThread();

        int c = getState(); 

        if (c == 0) {

            if (compareAndSetState(0, acquires)) { //非公平锁只要判断锁是空闲的,就直接尝试CAS获取锁,而不会判断自己是否需要排队

                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; //加锁失败

    }

}

比较会发现,公平锁和非公平锁加锁时,唯一的区别就是:公平锁有hasQueuedPredecessors()判断是否需要排队;而非公平锁只要判断锁是空闲的就直接尝试CAS获取锁,而不会判断自己是否需要排队。

ReentrantLock加锁时,如果通过CAS方式成功将state从0改为1(即获取了锁)就会调用setExclusiveOwnerThread()方法,setExclusiveOwnerThread()方法在AQS的父类AbstractOwnableSynchronizer中:

public abstract class AbstractOwnableSynchronizer {

   private transient Thread exclusiveOwnerThread;

    protected final void setExclusiveOwnerThread(Thread thread){

        exclusiveOwnerThread = thread;

    }

    protected final Thread getExclusiveOwnerThread(){

        return exclusiveOwnerThread;

    }

}

exclusiveOwnerThread变量代表当前拥有锁的线程。

 

总结ReentrantLock加锁原理:

①通过lock()方法加锁,调用lock()方法后内部会以CAS原子方式去尝试修改AQS中持有的state变量,即判断当前锁的状态,只有在state等于0时,锁才是空闲的。

②如果state=0,非公平锁会立刻去尝试获取锁;而公平锁并不能立刻加锁,还要判断等待队列中有没有等待的线程,如果没有等待的线程则直接尝试加锁,如果有等待的线程则优先从队列中获取线程加锁,并把当前线程入队列。

此时如果线程获取锁成功,就会将当前AQS中的exclusiveOwnerThread更新为当前线程对象,以便于后期锁重入时直接返回获取锁成功。

如果最开始修改失败,则调用acquire()方法去获取锁。其方法会内部尝试获取锁一次 ,并且将当前线程添加到等待队列中,然后通过CAS的方式不断自旋,一直获取前驱节点, 如果前驱节点是 head头节点就说明当前节点在队列首部,就尝试获取锁,如果获取成功则更新队列头节点为当前node,并移除当前遍历的前驱节点。如果获取锁失败,则通过前驱节点判断当前线程是否被阻塞,如果当前线程已经被中断,则更新标记位,并且暂停此循环,等待唤醒。

③如果state>0,说明锁被其他线程持有,持有该锁的线程在exclusiveOwnerThread属性中关联。判断exclusiveOwnerThread==currentThread是否成立,如果成立说明当前线程持有该锁,则对state累加1(实现锁重入);如果锁不是当前线程持有,则把该线程加入到队列中等待。

b327e5910bb84abbaa656676d9228670.png

(3)ReentrantLock释放锁

ReentrantLock释放锁会调用unlock()方法,其实就是state -1,并设置owner线程为null ,释放成功就唤醒下一个节点。

public void unlock() {

    sync.release(1);

}

ReentrantLock的unlock()方法会调用sync的release()方法,release()的具体实现在AQS里,公平锁和非公平锁的release一样的。在release()方法中,tryRelease()是模板方法。

protected final boolean tryRelease(int releases) {

    int c = getState() - releases; //减去要释放的数量后的锁状态

    //如果释放锁的线程不是拥有锁的线程则抛异常

    if (Thread.currentThread() != getExclusiveOwnerThread())

        throw new IllegalMonitorStateException();

    boolean free = false;

    if (c == 0) { //释放后锁状态变为空闲

        free = true;

        setExclusiveOwnerThread(null);  //修改当前拥有锁的线程为null

    }

    setState(c);//修改锁状态

    return free;

}

tryRelease()方法尝试释放当前锁,返回释放后锁资源是不是空闲(true空闲,false不空闲)。如果锁空闲的话会调用unparkSuccessor方法唤醒头节点的下一个节点。

 

5.ReentrantReadWriteLock读写锁

ReentrantReadWriteLock是一个可重入读写锁,内部提供了读锁和写锁的单独实现。其中读锁用于只读操作,可被多个线程共享;写锁用于写操作,只能互斥访问。

ReentrantReadWriteLock适合读多写少的应用场景。在一些业务场景中,大部分只是读数据,写数据很少,如果这种场景下依然使用独占锁会大大降低性能。因为独占锁会使得本该并行执行的读操作变成了串行执行。

读写锁维护着一对锁:一个读锁和一个写锁。通过分离读锁和写锁,使得并发性比一般的排他锁有了较大的提升:在同一时间可以允许多个读线程同时访问,但是在写线程访问时,所有读线程和其他的写线程都会被阻塞。

读写锁还是沿用AQS中的state变量,高16位表示读锁的数量,低16位表示写锁的数量。

78e8d511243f4ce9a2d829f775951820.jpg

(1)第一个问题:

为什么不用两个变量表示?假设用两个变量。int writeState表示写锁状态。int readState表示读锁状态。

d5617eee26324a509fe75496ae253e68.jpg

因为分成了两个操作,这两个操作不是原子性的,所以造成了上述问题。那能不能再引入一个锁?比如:

a5c28346dbda445d9ff820bfc100af89.jpg

但是这样不就使得读锁变成互斥锁了么?就不可以并发获取读锁的能力了。

所以最后读写锁用一个变量32位的state来表示,state的高16位表示读锁,低16位表示写锁。

(2)第二个问题:

怎么表示每个线程持有的读锁数量?

state变量的高16位表示的是所有线程持有的读锁数量总量。如果要表示每个线程各自持有的变量,就需要用到ThreadLocal。即源码中的这个变量

935d1c4766b44912b326d11619b0cefb.jpg

假设一段时间内,永远只有一个线程去获取读锁,那么就没有必要去初始化ThreadLocal,所以读写锁中有一个first变量。用来记录第一个获取读锁的线程以及持有锁的数量。

7af30dd3433a4c099c5eac1728307d76.jpg

读写锁的主要特性:

①公平性:支持非公平(默认)和公平的锁获取方式,其中非公平性能优于公平。

②重入性:支持重入。即读线程获取读锁之后能够再次获取读锁;写线程获取写锁之后能够再次获取写锁,同时也可以获取读锁。

重入时不支持锁升级,即同一个线程获取读锁后,直接申请写锁是不能获取成功的

重入时支持降级,即同一个线程获取写锁后,直接申请读锁是可以成功的。

 

(3)问题三:线程局部计数器

Sync类定义了一个线程局部变量readHolds,用于保存当前线程重入读锁的次数。如果该线程的读锁数减为0,则将该变量从线程局部域中移除。

static final class HoldCounter {

    int count = 0; // 这里使用线程的id而非直接引用,是为了方便GC

    final long tid = getThreadId( Thread.currentThread());

}

内部类,用于记录当前线程重入读锁的次数 。

static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> {

    public HoldCounter initialValue() {

        return new HoldCounter();

    }

内部类,继承自ThreadLocal,该类型的变量是每个线程各自保存一份,其中保存的是HoldCounter对象,用set方法保存,get方法获取

private transient ThreadLocalHoldCounter readHolds;

由于readHolds变量是线程局部变量(继承ThreadLocal类),每个线程都会保存一份副本,不同线程调用其get方法返回的HoldCounter对象不同。

readHolds中的HoldCounter变量保存了每个读线程的重入次数,即其持有的读锁数量。这么做的目的是便于线程释放读锁时进行合法性判断:线程在不持有读锁的情况下释放锁是不合法的,需要抛出IllegalMonitorStateException异常。

(4)缓存

Sync类定义了一个HoldCounter变量cachedHoldCounter,用于保存最近获取到读锁的线程的重入次数。

private transient HoldCounter cachedHoldCounter;

设计该变量的目的是:将其作为一个缓存,加快代码执行速度。因为获取、释放读锁的线程往往都是最近获取读锁的那个线程,虽然每个线程的重入次数都会使用readHolds来保存,但使用readHolds变量会涉及到ThreadLocal内部的查找,这是存在一定开销的。有了cachedHoldCounter这个缓存后,就不用每次都在ThreadLocal内部查找,加快了代码执行速度,相当于用空间换时间。

 

1)读写锁的用法

以缓存为例,多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行。但是如果有一个线程想去写共享资源,就不应该再有其他线程可以对该资源进行读或写操作。

缓存是⼀个键值对,读缓存时get()⽅法使⽤ReentrantReadWriteLock.ReadLock(),写缓存时put()⽅法使⽤ReentrantReadWriteLock.WriteLock()。这样就可以避免写被打断,实现多个线程同时读。

class Cache {

    private Map<String, Object> map = new ConcurrentHashMap<>();

    private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    //写缓存

    public void put(String key, Object value){

        readWriteLock.writeLock().lock();

        try {

            Log.e(TAG,key+"正在写入缓存: "+ value);

            TimeUnit.SECONDS.sleep(1);//模拟耗时

            map.put(key, value);

            System.out.println("写入缓存完成");

        } finally {

            readWriteLock.writeLock().unlock();

        }

    }

    //读缓存

    public Object get(String key){

        Object result = null;

        readWriteLock.readLock().lock();

        try {

            Log.e(TAG, key + "正在读取缓存:");

            TimeUnit.SECONDS.sleep(1);//模拟耗时

            result = map.get(key);

            System.out.println(key+"读取缓存完成");

        } finally {

            readWriteLock.readLock().unlock();

        }

        return result;

    }

}

多线程调用读写方法:

for (int i = 1; i <= 5; i++) {

    final int temp = i;

    new Thread(()->{

        cache.put("线程名"+temp, "数据"+temp);

    }, String.valueOf(i)).start();

}

for (int i = 1; i <= 5; i++) {

    final int temp = i;

    new Thread(()->{

        cache.get("线程名"+temp);

    }, String.valueOf(i)).start();

}

2)读写锁源码分析

(1)构造方法

ReentrantReadWriteLock有五个内部类,五个内部类之间也是相互关联的。

331263ecdff14f5bbb10a20372ecd7d7.png

ReentrantReadWriteLock有两个变量,分别存放读锁和写锁:

public class ReentrantReadWriteLock implements ReadWriteLock {

  private final ReentrantReadWriteLock.ReadL ock readerLock; //读锁

  private final ReentrantReadWriteLock.WriteL ock writerLock; //写锁

    final Sync sync; 

    public ReentrantReadWriteLock() {

        this(false); //默认为非公平模式

    }

    public ReentrantReadWriteLock(boolean fair) {

        sync=fair?new FairSync():new NonfairSync();

        readerLock = new ReadLock(this); //内部把ReentrantReadWriteLock中的sync传递给ReadLock的sync

        writerLock = new WriteLock(this);//内部把ReentrantReadWriteLock中的sync传递给WriteLock的sync

    }

}

(2)写锁(可重入的排他锁)

public static class WriteLock implements Lock, java.io.Serializable {

    private final Sync sync;

    protected WriteLock( ReentrantReadWriteLock lock) {

        sync = lock.sync; //获取ReentrantReadWriteLock的sync对象

    }   

}

①写锁加锁

public void lock() {

    sync.acquire(1);

}

public boolean tryLock( ) { //尝试获取锁

    return sync.tryWriteLock();

}   

写锁使用lock()方法加锁,lock()调用AQS里的acquire(1)方法:

AbstractQueuedSynchronizer.java:

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

这里与ReentrantLock一样,不同的是tryAcquire()的具体实现。在ReentrantReadWriteLock中tryAcquire方法():

protected final boolean tryAcquire(int acquires) {

    Thread current = Thread.currentThread();

    int c = getState();

    int w = exclusiveCount(c); //写线程数量(即获取独占锁的重入数)

    if (c != 0) { //锁不空闲,即有线程加锁(可能是读锁可能是写锁)

        //有线程加了读锁但没有线程加写锁,或者线程加的是写锁但加锁的线程不是当前线程,则直接返回false,无法获取写锁

        if (w == 0 || current != getExclusiveOwnerThread())

            return false;

        //有线程加了写锁,且就是当前线程加的,则可以获取写锁(重入),但是需要判断是否超过最高写线程数量

        if (w + exclusiveCount(acquires) > MAX_COUNT)

            throw new Error("Maximum lock count exceeded");

        setState(c + acquires);  //写锁重入成功

        return true;

    }

    //能够执行到这说明state为0,表示当前没有任何线程加读锁或写锁

    if (writerShouldBlock() || !compareAndSetState(c, c + acquires))

        return false;

    //当前线程不阻塞并且CAS成功获取到写锁,则设置写锁的持有线程为当前线程

    setExclusiveOwnerThread(current);

    return true;

}

该方法除了重入条件(当前线程为获取了写锁的线程)之外,增加了一个读锁是否存在的判断。如果存在读锁,则写锁不能被获取,原因在于:读写锁要确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。因此只有等待其他线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦被读取,则其他读写线程的后续访问均被阻塞。

其中,writerShouldBlock()方法表示当前线程是否需要阻塞,false表示不需要阻塞直接获取锁,true表示要阻塞无法获取锁。该方法在公平锁和非公平锁中的实现不同。

static final class FairSync extends Sync {

    final boolean writerShouldBlock() {

        return hasQueuedPredecessors();

    }

    final boolean readerShouldBlock() {

         return hasQueuedPredecessors();

   }

}

static final class NonfairSync extends Sync {

   final boolean writerShouldBlock() {

        return false;

    }

    final boolean readerShouldBlock() {

       return apparentlyFirstQueuedIsExclysive();

   }

}

如果是非公平锁,则writerShouldBlock始终返回false,因为不需要排队,非公平策略下可直接参与抢锁;如果是公平锁,则需要查看当前队列中是否存在其他排队的线程,如果存在则返回true,当前线程不能获取写锁,否则返回false可直接CAS获取锁。

写锁是一个支持重进入的排他锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态。

②写锁释放锁

public void unlock() {

    sync.release(1);

}

写锁使用unlock()方法释放锁,unlock()调用AQS里的release(1)方法:

AbstractQueuedSynchronizer.java:

public final boolean release(int arg) {

    if(tryRelease(arg)) {

        Node h = head; 

        if(h != null && h.waitStatus != 0) 

            unparkSuccessor(h); 

        return true; //释放锁成功

    }

    return false; //释放锁失败

}

其中tryRelease()是模板方法,在ReentrantReadWriteLock中,该方法的实现为:

protected final boolean tryRelease(int releases){

    if(!isHeldExclusively()) //如果写锁的持有者不是当前线程,直接抛出异常

        throw new IllegalMonitorStateException();

    int nextc = getState() - releases;//计算释放后的写锁数

    //free表示写锁是否被完全释放

    boolean free = exclusiveCount(nextc) == 0;

    if(free)//如果写锁状态为0,表示写锁被完全释放,即独占模式被释放

        setExclusiveOwnerThread(null);

    setState(nextc);

    return free; //返回独占模式是否被释放

}

写锁的释放与ReentrantLock的释放过程基本类似,每次释放均减少写状态,当写状态为0时表示写锁已被释放,从而等待的读写线程能够继续访问读写锁,同时前次写线程的修改对后续读写线程可见

(3)读锁—可重入的共享锁

读锁是一个可重入的共享锁,它能够被多个线程同时持有,在没有其他写线程访问时,读锁总是会获取成功。

①读锁加锁

读锁的加锁调用ReadLock内部类中的lock方法:

public void lock() {

    sync.acquireShared(1);

}

调用AQS的acquireShared()方法:

public final void acquireShared(int arg) {

    if (tryAcquireShared(arg) < 0)

        doAcquireShared(arg);

}

其中tryAcquireShared是模板方法,由子类具体实现。

ReentrantReadWriteLock的tryAcquireShared方法:

protected final int tryAcquireShared(int unused) {

    Thread current = Thread.currentThread();

    int c = getState(); 

    //exclusiveCount获取写线程数量。如果存在写锁,且持有锁的线程不是当前线程,则获取失败,返回-1

    if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)

        return -1;

    int r = sharedCount(c);   //原读锁数量

    //同时满足下面三个条件:①读线程不需要等待;②读锁数量小于最大数量;③CAS获取同步状态成功。则说明获取读锁成功

    if(!readerShouldBlock() && r < MAX_COUNT &&compareAndSetState(c, c+SHARED_UNIT)){

        if (r == 0) { //当前线程是第一个读线程

            firstReader = current; 

            firstReaderHoldCount = 1;  //第一个读线程占用的资源数为1

        } else if (firstReader == current) {//原读锁数量不为0,且第一个读线程即为当前线程,更新该线程占用的资源数,即锁重入

            firstReaderHoldCount++; 

        }  else { //原读锁数量不为0,且第一个读线程不为当前线程

            HoldCounter rh = cachedHoldCounter; //获取计数器

            if (rh == null || rh.tid != LockSupport.ge tThreadId(current)) //计数器为空或者计数器的tid不为当前线程的tid

                cachedHoldCounter = rh = readHolds.get();// 获取当前线程对应的计数器

            else if (rh.count == 0) //当前线程的计数为0

                readHolds.set(rh);//设置readHolds中值为当前线程的计数器

            rh.count++;  //count加1

        }

        return 1;

    }

    //如果上面三个条件有一个不满足

    return fullTryAcquireShared(current);

}

其中readerShouldBlock用于判断当前读线程是否需要等待。在公平锁和非公平锁中有不同的实现。

非公平锁中readerShouldBlock方法:

final boolean readerShouldBlock() {

    return apparentlyFirstQueuedIsExclusive();

}

boolean apparentlyFirstQueuedIsExclusive() {

    Node h, s;

    return (h = head) != null && (s = h.next) != null && !s.isShared() && s.thread != null;

}

公平锁的readerShouldBlock方法:

final boolean readerShouldBlock() {

    return hasQueuedPredecessors();

}

判断等待队列中是否有线程在排队。

tryAcquireShared方法最后一行表示在不满足获取读锁条件时会调用fullTryAcquireShared方法:

final int fullTryAcquireShared(Thread current) {

    HoldCounter rh = null;

    for (;;) {

        int c = getState();

        if (exclusiveCount(c) != 0) {//写锁数量不为0

            if (getExclusiveOwnerThread() != current) //不是当前线程持有写锁,直接返回-1

                return -1;

        } else if (readerShouldBlock()) { //写锁数量为0,且读线程需要被阻塞

            if (firstReader == current) {  //当前线程是第一个读线程,不操作

                // assert firstReaderHoldCount > 0;

            } else { //当前线程不是第一个读线程

                if (rh == null) {

                    rh = cachedHoldCounter;

                    if (rh == null || rh.tid != getThreadId(current)) { //计数器还是为空或者计数器的tid不为当前正在运行的线程的tid

                        rh = readHolds.get();

                        if (rh.count == 0)

                            readHolds.remove();

                    }

                }

                if (rh.count == 0)

                    return -1;

            }

        }

        if (sharedCount(c) == MAX_COUNT) //读锁数量大于最大值,直接抛出异常

            throw new Error("Maximum lock count exceeded");

        if (compareAndSetState(c, c + SHARED_UNIT)) {

            if (sharedCount(c) == 0) { //读线程数量为0,逻辑同上

                firstReader = current;

                firstReaderHoldCount = 1;

            } else if (firstReader == current) {

                firstReaderHoldCount++;

            } else {

                if (rh == null)

                    rh = cachedHoldCounter;

                if (rh == null || rh.tid != getThreadId(current))

                    rh = readHolds.get();

                else if (rh.count == 0)

                    readHolds.set(rh);

                rh.count++;

                cachedHoldCounter = rh; 

            }

            return 1;

        }

    }

}

在tryAcquireShared函数中,如果下列三个条件不满足(读线程是否应该被阻塞、小于最大值、比较设置成功)则会进行fullTryAcquireShared函数中,它用来保证相关操作可以成功。

 

tryAcquireShared的返回值说明:

负数:获取失败,线程会进入同步队列阻塞等待;

0:获取成功,但是后续以共享模式获取的线程都不可能获取成功;

正数:获取成功,且后续以共享模式获取的线程也可能获取成功

 

firstReader、firstReaderHoldCount分别用于记录第一个获取到写锁的线程及其持有读锁的数量;

cachedHoldCounter用于记录最后一个获取到写锁的线程持有读锁的数量;

readHolds是一个线程局部变量(ThreadLocal变量),用于保存每个获得读锁的线程各自持有的读锁数量;

 

tryAcquireShared的流程如下:

1、如果其他线程持有写锁,那么获取失败(返回-1);

2、否则,根据公平性判断是否应该阻塞。如果不用阻塞且读锁数量未饱和,则CAS请求读锁。如果CAS成功,获取成功(返回1),并记录相关信息;

3、如果根据公平性判断应该阻塞,或者读锁数量饱和,或者CAS竞争失败,那么交给完整版本的获取方法fullTryAcquireShared去处理;

其中步骤2如果发生了重入读(当前线程持有读锁的情况下,再次请求读锁),但根据公平性判断该线程需要阻塞等待,而导致重入读失败。按照正常逻辑重入读不应该失败。不过tryAcquireShared并没有处理这种情况,而是将其放到了fullTryAcquireShared中进行处理。此外CAS竞争失败而导致获取读锁失败,也交给fullTryAcquireShared去处理。

fullTryAcquireShared方法是尝试获取读锁的完全版本,用于处理tryAcquireShared方法未处理的:CAS竞争失败或因公平性判断应该阻塞而导致的重入读失败这两种情况。

fullTryAcquireShared其实和tryAcquire存在很多的冗余之处,但这么做的目的主要是让tryAcquireShared变得更简单,不用处理复杂的CAS循环。

fullTryAcquireShared主要是为了处理CAS失败和readerShouldBlock判true而导致的重入读失败,这两种情况在理论上都应该成功获取锁。fullTryAcquireShared的做法就是将这两种情况放在for循环中,一旦发生就重新循环,直到成功为止。

②读锁的释放

读锁的释放调用ReadLock内部类的unlock方法:

public void unlock() {

    sync.releaseShared(1);

}

调用AQS的releaseShared()方法:

public final boolean releaseShared(int arg) {

    if (tryReleaseShared(arg)) {

        doReleaseShared();

        return true;

    }

    return false;

}

其中tryReleaseShared是模板方法,由子类具体实现。

ReentrantReadWriteLock的tryReleaseShared()方法:

protected final boolean tryReleaseShared(int unused) {

    Thread current = Thread.currentThread();

    if (firstReader == current) { //当前线程是第一个读线程

        if (firstReaderHoldCount == 1)  //当前线程的计数为1,释放后需要清除

            firstReader = null;

        else //否则直接-1

            firstReaderHoldCount--;

    }  else {//当前线程不是第一个读线程

        HoldCounter rh = cachedHoldCounter; //获取缓存的计数器

        if (rh == null || rh.tid != getThreadId(current)) //计数器为空或者计数器的tid不为当前正在运行的线程的tid

            rh = readHolds.get(); //获取当前线程的计数器

        int count = rh.count;  //获取计数

        if (count <= 1) {  //计数小于等于1,则需要移除该线程对应的计数器

            readHolds.remove();

            if (count <= 0)  //计数小于等于0,直接抛出异常

                throw unmatchedUnlockException();

        }

        --rh.count; //计数减一

    }

    for (;;) {

        int c = getState(); 

        int nextc = c - SHARED_UNIT;

        if (compareAndSetState(c, nextc)) 

            //CAS更成功后返回状态是否是0,即锁是否完全被释放

            return nextc == 0;

    }

}

该方法返回锁(共享锁+独占锁)是否完全被释放。

 

 

 

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值