简单看看LockSupport和AQS

  此次咱们能够看看并发中锁的原理,大概会说到AQS,ReentrantLock,ReentrantReadWriteLock以及JDK8中新增的StampedLock,这些都是在java并发中很重要的东西,慢慢看吧!java

 

一.LockSupport工具类数组

  LockSupport工具类是jdk中rt.jar里面的,主要做用是挂起和唤醒线程,该类是建立锁和建立其余同步类的基础。还有咱们要知道,LockSupport这个类是以Unsafe这个类为基础,讲过前面简单的看了看Unsafe,是否是以为仍是比较熟悉的吧!多线程

  咱们先看看LockSupport的park(英文翻译:停下,坐下)和unpark(英文翻译:唤醒,启动)方法,注意,这两个方法和wait和notify功能很像,可是在这里我更喜欢叫作受权并发

  简单的看一个例子:ide

package com.example.demo.study;
import java.util.concurrent.locks.LockSupport;

public class Study0130 {

    public static void main(String[] args) {
        System.out.println("main begin");                
        LockSupport.park();       
        System.out.println("main end");
    }
}

 

  咱们能够看到咱们直接调用park方法的话,当前的线程就阻塞了,不能到后面去了,这里咱们能够说当前线程没有被LockSupport类受权,没有许可证,因此到这里碰到park()这个路口就只能挂了;那么怎么样才能使得当前线程被受权呢?咱们就须要unpark()方法进行受权工具

package com.example.demo.study;
import java.util.concurrent.locks.LockSupport;

public class Study0130 {

    public static void main(String[] args) {
        //这里就是给当前线程受权了,当前线程能够随便跑,碰到park都不会挂
        LockSupport.unpark(Thread.currentThread());
        
        System.out.println("main begin");
        LockSupport.park();
        System.out.println("main end");

    }
}

 

  还记得之前的wait和notify的用法么?一个线程A中调用了wait方法,那么线程A就挂起了,若是在线程B中调用notify方法,那么A线程就会被唤醒;这里的park和unpark方法也能够实现这种,看如下代码:ui

package com.example.demo.study;

import java.util.concurrent.locks.LockSupport;

public class Study0130 {

    public static void main(String[] args) {

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread1 start");
                //线程1会阻塞
                LockSupport.park();
                System.out.println("thread1 end");
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread2 start");
                //给线程1受权
                LockSupport.unpark(thread1);
                System.out.println("thread2 end");
            }
        });
        thread1.start();
        thread2.start();
    }
}

 

  咱们打开LockSupport的park和unpark方法能够发现,就是调用的Unsafe实现的,惋惜看不到源码...this

 

  假如咱们调用park方法使得线程阻塞过久了也不是咱们想看到的,咱们还可使用parkNanos设置阻塞时间,当时间到了,就会自动返回:spa

 

  最后说一下,还能够调用park方法的时候传进去一个对象,好比LockSupport.park(this);这样使用可使用jstack pid命令查看堆栈信息的时候,能够看到是那个类被阻塞了!线程

   到此为止,应该就是LockSupport的经常使用方法了!

 

二.认识AQS

  AQS全称是AbstractQueuedSynchronizer,叫作抽象同步队列,用于实现各类同步组件,好比并发包中的锁就是用这个实现的,把这个弄清楚了,那些锁的机制就差很少懂了!

  那么所谓的AQS究竟是什么呢?其实就是一个有顺序的双向链表(或者叫作FIFO双向队列,同样的意思),在这个双向链表中,每个节点中均可以存放一个线程,节点的全部属性以下图所示,咱们随便说几个;

  prev表示指向前一个节点,next指向后一个节点,thread表示当前节点存储的一个线程,SHARED表示当前节点存储的线程是因为获取共享资源是被阻塞了才被丢到链表中的;EXCLUSIVE表示当前节点存储的线程是因为获取独占资源阻塞才被丢到链表中来的;

  waitStatus表示当前节点存储的线程的状态,可能的状态有如下几种:(1)CANCELLED =  1;  表示线程被取消了  (2)SIGNAL    = -1; 表示线程须要唤醒 (3)CONDITION = -2;表示线程在链表中等待 (4)PROPAGATE = -3;表示线程释放共享资源时须要通知其余节点;

  注意,这里其实还有一个状态,就是waitStatus为0,表示当前节点是初始状态,因此能够知道当waitStatus大于0的时候是无效状态,小于零才是有效状态

  

  这个Node类是AQS的一个内部类,那么怎么经过AQS来访问这个链表呢?下面咱们再来看看AQS有哪些属性能够帮助咱们访问这个双向链表;

//字段 //指向链表的头节点
private transient volatile Node head;
//指向链表的尾节点
private transient volatile Node tail;
//状态信息,这个字段在每一个实现类中表达的意思都不同,好比在ReentrantLock中表示可重入的次数,
//在Semaphore中表示可用信号的个数等等用法
private volatile int state;

//获取Unsafe对象,前面用过的,还记得说过为何可使用getUnsafe的方式获取对象,而咱们本身的类中却不能用这种方式
private static final Unsafe unsafe = Unsafe.getUnsafe();

//下面的这几个属性就是获取AQS类中的字段的偏移量,在前几篇的博客已经说过了这偏移量有什么用
private static final long stateOffset;
private static final long headOffset;
private static final long tailOffset;
private static final long waitStatusOffset;
private static final long nextOffset;

//方法 //这几个方法都是尝试获取锁
public final void acquire(int arg) {}//独占方式 protected boolean tryAcquire(int arg) {}
public final void acquireShared(int arg) {}//共享方式 public final void acquireInterruptibly(int arg){}//独占方式
public final void acquireSharedInterruptibly(int arg){}//共享方式
//这几个方法都是试图释放锁 public final boolean release(int arg) {}//独占方式 public final boolean releaseShared(int arg) {}//共享方式 protected boolean tryRelease(int arg) {} protected boolean tryReleaseShared(int arg) {}

 

  在AQS中对线程的同步主要的是操做state,对state的操做方式分为两种,独占方式和共享方式,至于两种方式各自的获取锁和释放锁的方法在上面已经标识出来了!

  这里稍微提一下什么叫作锁啊?在java多线程中能够把一个对象当作一个锁,为何呢?咱们能够简单看看一个普通的java对象(不是数组)在java堆中有哪些组成部分:

 

   一个java对象是由对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)三部分组成,实例数据和对齐填充能够看作是一类,由于对齐填充就是起到填充空白的做用,由于java对象的字节数必须是8的倍数(对象头确定是8的倍数,这里其实就是填充实例数据成8的倍数便可),因此对齐填充可能有也可能没有;

  对象头通常有两部分组成(数组的话还有一个部分,即数组长度),以下所示:

    第一部分:用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为“MarkWord”。

    第二部分:对象头的另一部分是klass类型指针,即对象指向它的类元数据的指针,虚拟机经过这个指针来肯定这个对象是哪一个类的实例。

  咱们能够看做一个对象就是一个锁,若是一个线程获取了某个锁,那么在这个锁对象的对象头的markword中存了某个线程的编号,也就表示该线程持有了该锁!

  上面说了这么多,咱们大概就知道了所谓的AQS就是以下图所示这样,维护了一个链表,每次只有头部的这个节点中的线程是运行的,当头部的线程因为某些缘由阻塞了或中断了,下一个线程才会尝试获取资源,重复如此

  而后咱们再来讲说一个线程以独占方式获取资源或者是共享方式获取资源;

 

三.独占方式

  当一个线程要以独占方式获取该资源,说得直白一点就是实现一个独占锁,相似synchorized代码块同样,对共享资源的操做都在这个代码块中,一个线程只有先获取这个锁才能进入到代码块中操做共享资源,其余线程尝试获取锁的时候,和这个锁中对象头的线程编号比较若是不同,那就只能将这个线程放到链表中存起来,而后该线程挂起来,等条件知足以后再唤醒,就是使用LockSupport的park和unpark方法实现的。

  就以ReentrantLock为例,一个线程获取到了ReentrantLock的锁以后,在AQS中就会首先使用CAS将state从0变为1,而后设置当前锁为本线程所持有;若是当前线程继续尝试获取锁,那么只会将state从1变为2,其余的没啥变化,这也叫作可重入次数;当若是其余线程去尝试获取锁的时候,那么发现锁对象的对象头中不是本身线程编号,因而就丢进了阻塞队列中挂起;

  1.当线程经过acquire(int arg)获取独占资源时: 

public final void acquire(int arg) {
     //1.tryAcquire方法没有实现,这个方法主要是留给具体子类去实现,经过具体场景去用CAS修改state的值,修改为功返回true,不然false
     //2.若是修改state的值失败,就会到第二个条件这里,这里会将当前线程封装成一个Node.EXCLUSIVE类型的节点,而后存到链表尾端,最后在acquireQueued方法内部会调用
      LockSupport.park(this);方法阻塞线程
//3.调用selfInterrupt方法中断当前的线程,为何要这样呢?由于一个线程在阻塞队列中等待,这时经过某种方式把它中断了,不会当即看到效果的,
   //只会在这个线程获取资源后再调用selfInterrupt方法将中断补上
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } //中断当前线程 static void selfInterrupt() { Thread.currentThread().interrupt(); }

 

  2.当线程经过release(int arg)释放独占资源时:

public final boolean release(int arg) {
    //tryRelease方法没有实现,子类根据具体场景是实现,其实就是修改state的值
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
        //这个方法在下面,里面会调用LockSupport.unpark(s.thread)方法激活阻塞队列中的一个节点的线程,而这个激活的线程会经过tryAcquire尝试当前的state是否知足本身的须要
     //知足条件的话就运行,不知足的话仍是会挂起 unparkSuccessor(h); return true; } return false; }

  经过简单的看了这获取资源和释放资源咱们能够看到底层仍是使用的Unsafe的park和unpark方法,还有就是tryAcquire()方法和tryRelease()方法须要在具体的子类本身实现,在其中就是对AQS中state的修改,子类还须要定义state这个状态值的增减是什么含义;

  例如ReentrantLock继承自AQS的实现中,state为0表示锁空闲,为1表示锁被占用,在重写tryAcquire()方法的时候,须要用CAS将state的值从0改成1,而且设置当前锁的持有者就是当前线程;而重写tryRelease()方法的时候,就须要用CAS将state的值从1改成0,而后设置当前锁的持有者为null

 

四.共享方式

  知道了独占方式以后,共享方式就简单了,什么叫作共享?同一时间能够有多个线程获取资源,这就叫作共享!!!

  一个线程尝试去获取资源成功后,此时另一个线程也能够直接用CAS去尝试获取资源,成功的话就修改,失败的话就丢进链表中存起来;例如Semaphore信号量,当一个线程经过acquire()方法获取信号量的时候,信号量知足条件就经过CAS去获取,不知足就将线程丢到链表里面;

  共享方式和前面的独占方式其实很像,咱们也来简单的看一看:

  1.当线程经过acquireShared(int arg)获取共享资源时:

 public final void acquireShared(int arg) {
    //tryAcquireShared方法也是没有实现,留给具体子类会根据实际状况实现,会设置state的值,设置成功就直接返回
    //设置失败的话就进入到doAcquireShared方法中,这个方法里会将当前线程封装为Node.SHARED类型的节点,而后放到阻塞队列的最后面
   //使用LockSupport.park(this)方法挂起本身
if (tryAcquireShared(arg) < 0) doAcquireShared(arg); }

 

  2.当线程经过releaseShared(int arg)释放共享资源时:

public final boolean releaseShared(int arg) {
    //tryReleaseShared方法由子类实现,修改state的值,尝试释放资源
    //释放资源成功的话,而后使用LockSupport.unpark(thread)去唤醒阻塞队列中的一个线程
    //激活的线程会使用tryReleaseShared查看当前state的值是否符合本身的须要,知足则激活,向下运行,不然仍是被放在AQS阻塞队列中挂起
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

  例如读写锁ReentrantReadWriteLock就是继承自AQS的实现,因为state是int类型的,32位,高16位表示获取读锁的次数,因此读锁的tryAcquireShared方法实现中,首先检查写锁是否被其余线程持有,是则返回false,不然就用CAS将state的高16位+1;在读锁的tryReleaseShared的实现中,内部使用CAS将state的高16位减一,成功的话就返回true,失败的话返回false