JDK源码——java.util.concurrent(二)

测试代码:
https://github.com/kevindai007/springboot_houseSearch/tree/master/src/test/java/com/kevindai/juc
 juc中的类太多,大分部又都需要些一个demo才能更好的理解,因此再开一篇


咱们首先开始研究LockSupport这个类,这个类是用来创建锁和其他同步工具类的基本线程阻塞原语.Java锁和同步器框架的核心AQS:AbstractQueuedSynchronizer,就是通过调用LockSupport.park()和LockSupport.unpark()实现线程的阻塞和唤醒的.LockSupport通过底层unsafe提供park,unpark操作.简单点说:底层维护一个二义性的变量来保存一个许可,需要注意的是这个许可是一次性的,unpark操作设置该值为1,park操作检查该值是否为1,为1直接返回,不为1,,则阻塞
这个类的代码都是很简单的调用unsafe类的方法,没什么好分析的,咱们主要看下他的使用

public class LockSupportTest {
    public static void main(String[] args) throws InterruptedException {
        //主线程一直处于阻塞状态。因为许可默认是被占用的,调用park()时获取不到许可,所以进入阻塞状态
//        LockSupport.park();
//        System.out.println("block.");

        //多次unpark,只有一次park也不会出现什么问题,结果是许可处于可用状态
//        Thread thread = Thread.currentThread();
//        LockSupport.unpark(thread);//释放许可
//        LockSupport.unpark(thread);//释放许可
//        LockSupport.park();// 获取许可
//        System.out.println("b");


//        LockSupport是不可重入的,如果一个线程连续2次调用LockSupport.park(),那么该线程一定会一直阻塞下去
//        LockSupport.unpark(thread);
//
//        System.out.println("a");
//        LockSupport.park();
//        System.out.println("b");
//        LockSupport.park();
//        System.out.println("c");




        //线程如果因为调用park而阻塞的话,能够响应中断请求(中断状态被设置成true),但是不会抛出InterruptedException
        Thread t = new Thread(new Runnable()
        {
            private int count = 0;

            @Override
            public void run()
            {
                long start = System.currentTimeMillis();
                long end = 0;

                while ((end - start) <= 1000)
                {
                    count++;
                    end = System.currentTimeMillis();
                }

                System.out.println("after 1 second.count=" + count);

                //等待或许许可
                LockSupport.park();
                System.out.println("thread over." + Thread.currentThread().isInterrupted());

            }
        });

        t.start();

        Thread.sleep(2000);

        // 中断线程
        t.interrupt();


        System.out.println("main over");
    }
}

非常重要
下面来研究一下abstractQueuedSynchronizer(简称aqs),aqs是一个线程同步的框架,也是整个juc包的基础(Semaphore、CountDownLatch、ReentrantLock、ReentrantReadWriteLock等类均在其基础上完成的),下面咱们来看看其实现(为方便理解,部分逻辑需要引用ReentrantLock的代码来解释)

首先很容易发现aqs有三个很重要的属性:

    //头结点
    private transient volatile Node head;
    //尾节点
    private transient volatile Node tail;
    /**
     * The synchronization state.
     * 同步状态
     * state可以理解有多少线程获取了资源,即有多少线程获取了锁,初始时state=0表示没有线程获取锁.
     *独占锁时,这个值通常为1或者0
     *如果独占锁可重入时,即一个线程可以多次获取这个锁时,每获取一次,state就加1
     *一旦有线程想要获得锁,就可以通过对state进行CAS增量操作,即原子性的增加state的值
     *其他线程发现state不为0,这时线程已经不能获得锁(独占锁),就会进入AQS的队列中等待.
     *释放锁是仍然是通过CAS来减小state的值,如果减小到0就表示锁完全释放(独占锁)
     */
    private volatile int state;

下面来说一下aqs的大致逻辑

  • AQS维护了一个队列,并记录队列的头节点和尾节点
  • 队列中的节点是获取不到资源而阻塞的线程
  • AQS同样维护了一个状态,这个状态应该是判断线程能否获取到锁的依据,如果不能,就加入到队列
  • 当某个节点获取到资源后就移除队列,然后让其后面的节点尝试获取资源
    下面咱们来看看Node节点是如何实现的
volatile Node prev;//此节点的前一个节点。
volatile Node next;//此节点的后一个节点
volatile Thread thread;//节点绑定的线程。
volatile int waitStatus;//节点的等待状态
//节点状态:取消状态,该状态表示节点超时或被中断就会被踢出队列
static final int CANCELLED =  1;
//节点状态:等待触发状态,只有前一个节点的状态为SIGNAL时,当前节点的线程才能被挂起
static final int SIGNAL    = -1;
//节点状态:等待条件状态,表明节点对应的线程因为不满足一个条件(Condition)而被阻塞。
static final int CONDITION = -2;
//节点状态:状态需要向后传播,使用在共享模式头结点有可能处于这种状态,表示锁的下一次获取可以无条件传播
static final int PROPAGATE = -3;
//需要补充的而是0时新节点才会有的状态

可以看出Node维护了一个双向队列,,并且每个节点都有自己的状态

再看看AQS中定义的几个重要的方法:

public final void acquire(int arg);//请求获取独占式资源(锁)
public final boolean release(int arg);//请求释放独占式资源(锁)
public final void acquireShared(int arg);//请求获取共享式资源
public final boolean releaseShared(int arg);//请求释放共享式资源
//独占方式。尝试获取资源,成功则返回true,失败则返回false
protected boolean tryAcquire(int arg) {
  throw new UnsupportedOperationException();
}
//独占方式。尝试释放资源,成功则返回true,失败则返回false。
protected boolean tryRelease(int arg) {    
  throw new UnsupportedOperationException();
}
//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源
protected int tryAcquireShared(int arg) {    
  throw new UnsupportedOperationException();
}
protected int tryReleaseShared(int arg) {
  throw new UnsupportOperationException();
}

可以看到aqs用acquire()和release()方法提供对资源的获取和释放
但是try**()结构的方法都是只抛出了异常,很显然这类方法是需要子类去实现的.
这也因为AQS定义了两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可以同时执行,如Semaphone、CountDownLatch), AQS负责获取资源(修改state的状态),而自定义同步器负责就要实现上述方法告诉AQS获取资源的规则.

下面来分析分析这几个方法:
1、acquire(int)
此方法是aqs实现独占式资源获取的顶层方法,这个方法和ReentrantLock.lock()等有着相同的语义.下面我们开始看源码

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

这个函数共调用了4个方法, 其中tryAcquire(arg)是在子类Sync中实现, 其余在均是AQS中提供.
而这个方法的流程比较简单:
- tryAcquire()尝试获取资源,如果成功, 则方法结束
- addWaiter()方法以独占方式将线程加入队列的尾部
- acquireQueued()方法是线程在等待队列中等待获取资源
- selfInterrupt(), 如果线程在等待过程中被中断过,在这里相应中断.(线程在等待过程中是不响应中断的,只有获取资源后才能自我中断)

下面来一一解读这些方法:
(1)、tryAcquire()
此方法尝试去获取独占资源.如果获取成功,则返回true,否则返回false。tryAcquire()方法前面已经说过,这个方法是在子类中是实现的. 而在ReentrantLock中,这个方法也正是tryLock()的语义.如下是ReentrantLock对tryAcquire()实现的源码(ReentranLock中tryAcquire()与nonfairTryAcquire()一致):

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();    
    int c = getState();   
    if (c == 0) {//等于0表示当前锁未被其他线程获取到
      if (!hasQueuedPredecessors() //检查队列中是否有线程在当前线程的前面
                && compareAndSetState(0, acquires)) {//CAS操作state,锁获取成功
        setExclusiveOwnerThread(current); //设置当前线程为占有锁的线程
        return true;
      }   
    } else if (current == getExclusiveOwnerThread()) {//非0,锁已经被获取,并且是当前线程获取.支持可重入锁
      int nextc = c + acquires;       
      if (nextc < 0)
        throw new Error("Maximum lock count exceeded");    
      setState(nextc);  //更改状态位,
      return true;   
    }
    return false;//未能获取锁
}
public final boolean hasQueuedPredecessors() {    
  Node t = tail; 
  Node h = head;   
  Node s;    
  /**
   *如果h=t,则队列未被初始化,返回false
   *如果队列中没有线程正在等待, 返回true
   *如果当前线程是队列中的第一个元素, 返回true,否则返回false
   **/
  return h != t &&  ((s = h.next) == null || s.thread != Thread.currentThread());
}

(2)、sqc中addWaiter(int)
再看acquire()的第二个流程,获取锁失败, 则将线程加入队列尾部, 返回新加入的节点

private Node addWaiter(Node mode) {
  //以独占模式构建节点,节点有共享和独占两种模式
    Node node = new Node(Thread.currentThread(), mode);    
    Node pred = tail;  
    //如果pred不为空,说明有线程在等待
    //尝试使用CAS入列,如果入列失败,则调用enq采用自旋的方式入列
    //该逻辑在无竞争的情况下才会成功,快速入列  
    if (pred != null) {
        node.prev = pred;    //双向队列    
        if (compareAndSetTail(pred, node)) {//CAS更新尾部节点
           //将原tail节点的后节点设置为新tail节点
           //由于CAS和设置next不是原子操作,因此可能出现更新tail节点成功,但是未执行pred.next = node,导致无法从head遍历节点;
           //但是由于前面已经设置了prev属性,因此可以从尾部遍历;
           //像getSharedQueuedThreads、getExclusiveQueuedThreads都是从尾部开始遍历
          pred.next = node;  //双向队列
          return node;        
        }    
    }    
    enq(node);  //如果队列没有初始化活更新尾部节点失败,程序就会到这一步,通过自旋入列
    return node;
}
private Node enq(final Node node) {
    for (;;) {//自旋+CAS配合使用方式,一直循环知道CAS更新成功.
        Node t = tail; 
        if (t == null) {//队列为空, 没有初始化,必须初始化
            if (compareAndSetHead(new Node())) 
                tail = head;      
        } else { 
            node.prev = t; 
            if (compareAndSetTail(t, node)) { //设置尾节点,此时的head是头节点,不存放数据
                t.next = node; 
                return t;            
            }        
        }   
    }
}

(3)、sqc中acquireQueued()
addWaiter()完成后返回新加入队列的节点, 紧接着进入下一个流程acquireQueued(), 在这个方法中, 会实现线程节点的阻塞和唤醒. 所有节点在这个方法的处理下,等待资源

final boolean acquireQueued(final Node node, int arg) { 
    boolean failed = true;  //是否拿到资源
    try {        
        boolean interrupted = false;  //等待过程中是否被中断过
        for (;;) {        //又是一个自旋配合CAS设置变量
            final Node p = node.predecessor();       //当前节点的前驱节点  
            if (p == head && tryAcquire(arg)) {//如果前驱节点是头节点, 则当前节点已经具有资格尝试获取资源
                setHead(node);    //获取资源后,设置当前节点为头节点   
                p.next = null; // help GC                
                failed = false;   
                return interrupted;   
            }            
              //如果不能获取资源,就进入waiting状态
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())                
                interrupted = true;        
        }    
    } finally {        
        if (failed)            
            cancelAcquire(node);    
    }
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {    
    int ws = pred.waitStatus; //获取前一个节点的状态
    if (ws == Node.SIGNAL)
      /*
      *此时前驱节点完成任务后能够唤醒当前节
      *记住,唤醒当前节点的任务是前驱节点完成
      */
        return true;    
    if (ws > 0) { //ws大于0表示节点已经被取消,应该移出队列.               
        do {            
            //节点的前驱引用指向更前面的没有被取消的节点.所以被取消的节点没有引用之后会被GC
            node.prev = pred = pred.prev;        
        } while (pred.waitStatus > 0);        
        pred.next = node;    
    } else {      
        //找到了合适的前驱节点后,将其状态设置为SIGNAL
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);    
    }    
    return false;
}

接下来是 parkAndCheckInterrupt() 方法, 真正让节点进入waiting状态的方法,是在这个方法中调用的.

private final boolean parkAndCheckInterrupt() {  
    LockSupport.park(this);    //使线程进入waiting状态,查看上面的LockSupport类介绍
    return Thread.interrupted(); //检查是否被中断
}

(4)、selfInterrupt()
acquire()方法不是立即响应中断的. 由于线程获取同步状态失败加入到同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移除,而是在获取资源后进行自我中断处理

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

至此独占锁获取资源的过程已经分析完了,理解流程只有也并不十分复杂,简单来说就是尝试获取资源, 如果获取不到就进入等待队列变成等待状态

2、release(int)
讲了如何获取到资源,接下来就应该如何释放资源.这个方法会在独占的模式下释放指定的资源(减小state),此方法与ReentrantLock.unlock()有相同的语意

public final boolean release(int arg) {    
    if (tryRelease(arg)) {    //尝试释放资源
        Node h = head;        
        if (h != null && h.waitStatus != 0)            
            unparkSuccessor(h);   //唤醒队列的下一个节点
        return true;    
    }    
    return false;
}

分析释放资源流程
(1)、tryRelease()这个方法是在子类中实现的.我们以ReentrantLock.unlock()为例解读资源释放的过程

protected final boolean tryRelease(int releases) {    
    int c = getState() - releases;   //state减去指定的量, 
    if (Thread.currentThread() != getExclusiveOwnerThread())       
        throw new IllegalMonitorStateException();    
    boolean free = false;    
    if (c == 0) {  //独占锁模式下,state为0时表示没有线程获取锁,这时才算是当前线程完全释放锁
        free = true;        
        setExclusiveOwnerThread(null);    
    }    
    setState(c);    
    return free;
}

(2)、unparkSuccessor()
此方法用于唤醒后继节点

private void unparkSuccessor(Node node) { 
    int ws = node.waitStatus;    
    if (ws < 0)        
        compareAndSetWaitStatus(node, ws, 0);      
    Node s = node.next;    
    if (s == null || s.waitStatus > 0) {   //waitStatus表示节点已经被取消,应该踢出队列
        s = null;        
          //从后想前找到最靠前的合法节点
        for (Node t = tail; t != null && t != node; t = t.prev)            
            if (t.waitStatus <= 0)                
                s = t;    
    }    
    if (s != null)       
        LockSupport.unpark(s.thread);
}

至此独占锁的获取、释放资源流程都已经完了,我也是查的不少资料才把这个流程捋清楚,快给我点赞

上面分析了独占锁的流程,下面咱们接着类分析共享锁的过程
1、acquireShared()
此方法是aqs实现共享式资源获取的顶层方法.下面我们开始看源码

    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

这个函数共调用了2个方法, 其中tryAcquireShared(arg)是在子类Sync中实现, doAcquireShared则是AQS中提供.
方法流程很简单,首先尝试获取资源,如果状态小于0(未获取成功),则调用doAcquireShared()方法加入阻塞队列.下面咱们分别来看看这两个方法
(1)、tryAcquireShared()
tryAcquireShared()在aqs中仅是一个抽象方法,具体实现在子类中,这里我以CountdownLatch为例进行分析

        //等于0表示当前锁未被其他线程获取到,即当前线程获取到锁时返回1,否则返回-1
        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }

2、doAcquireShared()
共享模式获取的核心公共方法,咱们看看源码

 private void doAcquireShared(int arg) {
        //添加当前线程为一个共享模式的节点,addWaiter()方法在独占模式是分析过,在此不做重复分析
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {//如果前驱节点是头节点, 则当前节点已经具有资格尝试获取资源
                    int r = tryAcquireShared(arg);
                   //此时当state值大于0则认为获取成功
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                //如果前驱节点不是头节点则不能能获取资源,就进入waiting状态.判断当前节点是否应该被阻塞,是则阻塞等待其他线程release
                //此处的方法前面也分析过,在此不做研究
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            //如果出异常,没有完成当前节点的出队,则取消当前节点
            if (failed)
                cancelAcquire(node);
        }
    }
private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);//把当前节点设为头结点
        if (propagate > 0 || h == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())//如果后继节点为共享模式且参数propagate是否大于0或者PROPAGATE是否已被设置,则唤醒后继节点
                doReleaseShared();
        }
    }

这样共享锁的基本流程就结束了,简单来说就是尝试获取资源,如果获取不到就加入队列中等待.与独占锁不同的是,独占锁尝试获取资源时会检查队列中是否有其他线程,如果没有就设置当前线程为占有锁的线程,即只有一个线程持有资源;而共享模式当调用doAcquireShared时,会看后续的节点是否是共享模式,如果是,会通过doReleaseShared()唤醒后续节点,让所有等待的共享节点获取资源

下面来分析一下释放资源的过程
1、releaseShared()

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {//尝试释放资源
            doReleaseShared();
            return true;
        }
        return false;
    }

咱们直接去CountdownLatch中看看tryReleaseShared()方法

        protected boolean tryReleaseShared(int releases) {
            for (;;) {//自旋+cas改变状态
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }

再来看看doReleaseShared()方法,这是共享模式释放资源的核心方法

   private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {/如果等待队列中有等待线程
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    //此方法前面分析过,用于唤醒后续节点
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

此方法逻辑也比较简单,就是唤醒第一个等待节点.但需要注意的是,根据前面acquireShared的逻辑,被唤醒的线程会通过setHeadAndPropagate继续唤醒后续等待的线程

到这里AQS就分析完了,到这里应该对独占锁、共享锁有一个认识,不清楚没关系,后续咱们会在其实现类中结合实际情况,进行更加深入的分析,如果有什么想讨论的欢迎留言一起讨论

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值