java架构之路(多线程)AQS之ReetrantLock显示锁的使用和底层源码解读

  说完了咱们的synchronized,此次咱们来讲说咱们的显示锁ReetrantLock。java

上期回顾:node

  上次博客咱们主要说了锁的分类,synchronized的使用,和synchronized隐式锁的膨胀升级过程,从无锁是如何一步步升级到咱们的重量级锁的,还有咱们的逃逸分析。编程

锁的粗化和锁的消除安全

  这个原本应该是在synchronized里面去说的,忘记了,不是很重要,可是须要知道有这么一个东西啦。多线程

  咱们先来演示一下锁的粗化:并发

StringBuffer sb = new StringBuffer();

public void lockCoarseningMethod(){
    //jvm的优化,锁的粗化
    sb.append("1");

    sb.append("2");

    sb.append("3");

    sb.append("4");
}

  咱们都知道咱们的StringBuffer是线程安全的,也就是说咱们的StringBuffer是用synchronized修饰过的。那么咱们能够得出咱们的4次append都应该是套在一个synchronized里面的。app

StringBuffer sb = new StringBuffer();

public void lockCoarseningMethod() {
    synchronized (Test.class) {
        sb.append("1");
    }

    synchronized (Test.class) {
        sb.append("2");
    }
    synchronized (Test.class) {
        sb.append("3");
    }
    synchronized (Test.class) {
        sb.append("4");
    }
}

  按照理论来讲应该是这样的,其实JVM对synchronized作了优化处理,底层会优化成一次的synchronized修饰,感兴趣的能够用javap -c 本身看一下,这里就不带你们去看了,我之前的博客有javap看汇编指令码的过程。框架

StringBuffer sb = new StringBuffer();

public void lockCoarseningMethod() {
    synchronized (Test.class) {
        sb.append("1");
        sb.append("2");
        sb.append("3");
        sb.append("4");
    }
}

  再来看一下锁的消除,其实这个锁的消除,真的对于synchronized理解了,锁的消除一眼就知道是什么了。less

public static void main(String[] args) {
    synchronized (new Object()){
        System.out.println("开始处理逻辑");
    }
}

  对于synchronized而言,咱们每次去锁的都是对象,而你每次都建立的一个新对象,那还锁毛线了,每一个线程均可以拿到对象,均可以拿到对象锁啊,因此没不会产生锁的效果了。jvm

概述AQS:

  AQS是AbstractQueuedSynchronizer的简称,字面意思,抽象队列同步器。Java并发编程核心在于java.concurrent.util包而juc当中的大多数同步器 实现都是围绕着共同的基础行为,好比等待队列、条件队列、独占获取、共享获 取等,而这个行为的抽象就是基于AbstractQueuedSynchronizer简称AQS,AQS定 义了一套多线程访问共享资源的同步器框架,是一个依赖状态(state)的同步器。就是咱们上次博客说的什么公平锁,独占锁等等。

AQS具有特性
  • 阻塞等待队列
  • 共享/独占
  • 公平/非公平
  • 可重入
  • 容许中断

AQS的简单原理解读:

  ReetrantLock的内部功能仍是很强大的,有不少的功能,咱们来一点点缕缕。如Lock,Latch,Barrier等,都是基于AQS框架实现,通常经过定义内部类Sync继承AQS将同步器全部调用都映射到Sync对应的方法AQS内部维护属性volatile int state (32位),state表示资源的可用状态

  • State三种访问方式
  1. getState()
  2. setState()
  3. compareAndSetState()
  • AQS定义两种资源共享方式
  1. Exclusive-独占,只有一个线程能执行,如ReentrantLock
  2. Share-共享,多个线程能够同时执行,如Semaphore/CountDownLatch
  • AQS定义两种队列
  1. 同步等待队列
  2. 条件等待队列
  • AQS已经在顶层实现好了。自定义同步器实现时主要实现如下几种方法:
  1. isHeldExclusively():该线程是否正在独占资源。只有用到condition才须要去实现它。
  2. tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
  3. tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
  4. tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  5. tryReleaseShared(int):共享方式。尝试释放资源,若是释放后容许唤醒后续等待结点返回true,不然返回false。

  刚才提到那么多属性,可能会有一些懵,咱们来看一下ReentrantLock内部是怎么来实现哪些锁的吧。

  打开咱们的ReetrantLock源代码能够看到一个关键的属性

private final Sync sync;

  后面有一个抽象方法而且继承了AbstractQueuedSynchronizer类,内部有一个用volatile修饰过的整型变量state,他就是用来记录上锁次数的,这样就实现了咱们刚才的说的重入锁和非可重入锁。咱们来画一个图。

   AbstractQueuedSynchronizer这个类里面定义了详细的ReetrantLock的属性,后面我会一点点去说,带着解读一下源码(上面都是摘自源码的)。state和线程exclusiveOwnerThread比较好理解,最后那个队列可能不太好弄,我这里写的也是比较泛化的,后面我会弄一个专题一个个去说。 相面说的CLH队列其实不是很准确,咱们能够理解为就是一个泛型为Node的双向链表结构就能够了。

  等待队列中Node节点内还有三个很重要的属性就是prev前驱指针指向咱们的前一个Node节点,和一个next后继指针来指向咱们的下一个Node节点,这样就造成了一个双向链表的结构,于此同时还有一个Thread来记录咱们的当前线程。

  在条件队列中,prev和next指针都是null的,不论是什么队列,他都有一个waitStatus的属性来记录咱们的节点状态的,就是咱们刚才说的CANCELLED结束、SIGNAL可唤醒那四个常量值。

AQS中ReetrantLock的使用:

  公平锁和非公平锁:这个仍是比较好记忆的,举一个栗子,咱们去车站排队上车,总有**插队,用蛇形走位能够上车的是吧,这就是一个非公平的锁,若是说,咱们在排队的时候加上护栏,每次只能排一我的,他人没法插队的,这时就是一个公平锁。总之就是不加塞的就是公平的,咱们都讨厌不公平。

   重入锁与非可重入锁:这个也很好理解,重入锁就是当咱们的线程A拿到锁之后,能够继续去拿多把锁,而后再陆陆续续的作完任务再去解锁,非可重入呢,就是只能得到一把锁,若是想获取多把锁,很差意思,去后面排下队伍。下面我化了一个重入锁的栗子,快过年了,你们提着行李回老家,咱们进去了会一并带着行李进去(不带行李的基本是行李丢了),这就是一个重入锁的栗子,咱们人进去了得到通道经过(锁),而后咱们也拖着行李得到了通道经过(锁),而后咱们才空出通道供后面的人使用。若是是非可重入锁就是人进去就进去吧,行李再次排队,说不许何时能进来。

   上一段代码来验证一下咱们上面说的那些知识点。

import java.util.concurrent.locks.ReentrantLock;

public class Test {

    private ReentrantLock lock = new ReentrantLock(true);//true公平锁,false非公平锁


    public void lockMethod(String threadName) {
        lock.lock();
        System.out.println(threadName + "获得了一把锁1");

        lock.lock();
        System.out.println(threadName + "获得了一把锁2");

        lock.lock();
        System.out.println(threadName + "获得了一把锁3");

        lock.unlock();
        System.out.println(threadName + "释放了一把锁1");

        lock.unlock();
        System.out.println(threadName + "释放了一把锁2");

        lock.unlock();
        System.out.println(threadName + "释放了一把锁3");
    }


    public static void main(String[] args) {
        Test test = new Test();

        new Thread(() -> {
            String threadName = Thread.currentThread().getName();
            test.lockMethod(threadName);

        }, "线程A").start();
    }

}

   经过代码阅读咱们知道咱们弄一个重入锁,加三次锁,解三次锁,咱们来看一下内部sync的变化,调试一下。

    咱们看到了咱们的state变量是用来存储咱们的入锁次数的。刚才去看过源码的小伙伴知道了咱们的state是经过volatile修饰过的,虽然能够保证咱们的有序性和可见性,可是一个int++的操做,他是没法保证原子性的,咱们继续来深挖一下代码看看内部是怎么实现高并发场景下保证数据准确的。点击lock方法进去,咱们看到lock方法是基于sync来操做的,就是咱们上面的画的那个ReetrantLock的图。

/**
 * Sync object for fair locks
 */
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() &&//hasQueuedPredecessors判断是否有正在等待的节点,
                    compareAndSetState(0, acquires)) {//经过unsafe去更新上锁次数
                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;
    }
}

  此次咱们开启多个线程来同时访问来看一下咱们的Node的变化。同时开启ABCD四个线程来执行这个

   此次咱们看到了head属性和tail属性再也不是空的。head是也是一个node节点,前驱指针是空的,后驱指针指向后继节点,Thread为空,tail的node节点正好是和head相对应的节点。这样的设计就是为了更好的去验证队列中仍是否存在剩余的线程节点须要处理。而后该线程运行结束之后会唤醒在队列中的节点,然其它线程继续运行。

  咱们知道咱们建立的公平锁,若是说BCD好好的在排队,E线程来了,只能好好的去排队,由于公平,因此排队,若是咱们建立的是非公平锁,E线程就有机会拿到锁,拿到就运行,拿不到就去排队。

相关文章
相关标签/搜索