JUC--007--locks3

通过 CountDownLatch 来研究 AQS:
前面已经说过 CountDownLatch 唤醒的两种方式:还有第三种:
1: 正常唤醒, countDownLatch.countDown(); 让 倒计时的值达到0
2: interrupte, 通过线程打断的方式唤醒阻塞的线程。
3: countDownLatch.await(timeout)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
仍然从具体的代码开始:
子线程:1秒后超时阻塞
主线程:2秒后永久阻塞

boolean isZero = countDownLatch.await(6, TimeUnit.SECONDS);
这个方法的返回值是:超时时间到了返回时,是否已经倒计时为0
返回 true 表示,这个方法返回时,倒计时已经为0
返回 false 表示,时因为超时返回的

private static void t9() throws InterruptedException {
    CountDownLatch countDownLatch = new CountDownLatch(1);

    new Thread(()->{
        try {
            sleep(1000);
            System.out.println("等待中....");
            boolean isZero = countDownLatch.await(6, TimeUnit.SECONDS);
            System.out.println("----timeout-----:is zero" + isZero);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();

    sleep(2000);
    countDownLatch.await();
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    public boolean await(long timeout, TimeUnit unit)
        throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }
    
    public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        //如果当前倒计时已经是 0,tryAcquireShared(arg) 返回 1,|| 前面为true,后面不执行
        //如果当前倒计时还不是 0,tryAcquireShared(arg) 返回 -1,继续执行 || 后面的
        return tryAcquireShared(arg) >= 0 || doAcquireSharedNanos(arg, nanosTimeout);
    }

    private boolean doAcquireSharedNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (nanosTimeout <= 0L)
            return false;
        
        //System.nanoTime() 获取当前相对时间。 deadline 是未来到期的相对时间(纳秒)
        final long deadline = System.nanoTime() + nanosTimeout;
        final Node node = addWaiter(Node.SHARED); //添加节点,
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {                  //-----------D
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return true;    //-----------C
                    }
                }
                
                //距离到期(超时)还有多少纳秒
                nanosTimeout = deadline - System.nanoTime();
                if (nanosTimeout <= 0L)
                    /* 
                     * 1、在阻塞前,这里就成立, 说明指定的超时时间太短了。
                     *    比如指定超时时间 <= 0。 刚进来这里就超时了。不必阻塞,立刻返回
                     * 2、如果到期时间还剩不到 1000 纳秒,就不去休眠了,在这个 for 循环中
                     *    转两圈,1000纳秒就耗没了
                     * 3、阻塞超时以后,自动被唤醒,接着for寻,这时 if 判断一定小于0
                     */
                    return false;   //-----------B
                    
                /*
                 * shouldParkAfterFailedAcquire 自己想要休眠,那就把自己前节点状态改成 SIGNAL
                 * 如没有改成功,继续循环,直到改成功以后,才能执行 && 后的代码,才能休眠自己。
                 *
                 * nanosTimeout > spinForTimeoutThreshold :
                 * static final long spinForTimeoutThreshold = 1000L; 一个常量,表示1000纳秒
                 * 如果到期时间还剩不到 1000 纳秒, 就没有必要让线程休眠了。刚休眠就要唤醒。
                 * 如果到期时间还剩不到 1000 纳秒,&& 后面返回 false, if 进不去, 接着外层for
                 * 循环,有可能就从  上面 if (nanosTimeout <= 0L) {} 中出去了。
                 */
                if (shouldParkAfterFailedAcquire(p, node) &&
                    nanosTimeout > spinForTimeoutThreshold)
                    //来到这里说明,前节点已经设置状态 SIGNAL, 而且到期时间还足够的长,那就休眠吧
                    LockSupport.parkNanos(this, nanosTimeout);  //-----------A
                if (Thread.interrupted())
                    throw new InterruptedException();   //-----------E
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

    //LockSupport::parkNanos 阻塞线程
    public static void parkNanos(Object blocker, long nanos) {
        if (nanos > 0) {
            Thread t = Thread.currentThread();
            setBlocker(t, blocker);
            //有超时的阻塞,时间到后,自动返回, 如果 nanos < 0 , 立刻返回
            UNSAFE.park(false, nanos);
            setBlocker(t, null);
        }
    }
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
经过上面代码, 线程要么因为超时时间太短,直接返回了。要么就阻塞了。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
此时阻塞的线程想要解除阻塞也有几种途径:
1、超时时间到了,返回
   超时返回也是正常解除阻塞,而不是线程打断, 所以从 ----A 中解除阻塞后,
   后面异常不会抛出。接着进行 for 循环, 大概率的从 ----B 退出循环,返回到用户
   代码。但是也有很小可能从 ----C 处退出。条件是:自己的前节点是 head, 而且,
   刚刚从超时阻塞中唤醒,而倒计时也被设置成了0,就是这么巧,本来是超时退出的。
   结果变成了正常流程(倒计时达到了0)退出。 (这个小概率事件是正常流程,不再这里说了)
   
   接着还要执行 finally 中的代码 cancelAcquire(node); 把自己从链表中删除,同时
   根据情况看看有没有必要唤醒自己的下一个节点。 返回一步步返回到用户代码中。
   
2、阻塞超时前,倒计时达到了0。 
   让倒计时为0的那个线程,执行了 doReleaseShared()【或者那个小概率事件,自己阻塞超时
   被自动唤醒,然后,倒计时为0的线程在 执行 doReleaseShared 时,被自己抢先来一次for
   循环, 而自己正好又是 head 的下一个,此时 ----D 成立了, 这是他们可能会竞争执行
   doReleaseShared 不过后续的流程时相同的了】。从 head 开始,解除
   head 后一个线程的阻塞, head 的后一个线程开始运行,干掉head, 自己当head, 
   然后唤醒自己的下一个 .....
   本线程是的阻塞虽然还未timeout, 但是也提前唤醒了。从 ----C 开始退出,
   finally 中的代码 条件不满足,不执行
   
3、阻塞中等待超时前,被其他线程打断 【thread1.interrupt();】
   线程被打断,从阻塞中返回,----E 处成立,抛出异常,这个异常让 for 循环结束,
   而且异常在这里没有捕获,直接抛出用户代码,同时调用 finally 的代码,看看
   需不需要唤醒自己的下一个节点。(自己已经唤醒了,如果还需要自己唤醒下一个,那就也
   把下一个唤醒,下一个唤醒后如果发现自己还没有到达唤醒条件,就会再次进入休眠)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
CoundDownLatch 能看的也都看差不多了,但是对于 AbstractQueuedSynchronizer 的认识
还是没有认识到位,还不能总结出 AbstractQueuedSynchronizer 的意思。 所以还需要
从其他途径找到突破口。

在分析 CoundDownLatch 的时候添加节点,调用了方法:Node node = addWaiter(Node.SHARED);
传入的是 Node.SHARED ,共享模式。以现在的认知水平来结合 CoundDownLatch 说一下对
共享模式的认识。 CoundDownLatch 可以通过调用 await 方法将调用者阻塞。 有多少
个调用者都是可以的。而且所有的调用者全部阻塞(不知道这个阻塞可不可以理解成成功获取
了资源,如果说成锁的话,是不是都获得了锁,所以是共享的??)。所以想要了解什么是
共享的模式 还要 对照另外一个模式 独占模式。

搜索一下 addWaiter 的调用, 发现使用 Node.EXCLUSIVE 作为参数的有三个方法:
public final void acquire(int arg) 
private boolean doAcquireNanos(int arg, long nanosTimeout)
private void doAcquireInterruptibly(int arg)

一个公开的方法,两个私有方法,两个私有方法有分别被下面两个公开方法调用:
public final boolean tryAcquireNanos
public final void acquireInterruptibly(int arg)

然后搜索这三个公开的方法,发现他们都被 ReentrantLock 使用。
所以新的研究对象出现了: ReentrantLock
================================================================================
ReentrantLock:
ReentrantLock 一个可重入的独占锁。
可重入:当一个线程已经获取锁,那么再没有释放的情况下还能再次获取该锁。现在不明白的
  地方是,线程都已经获取锁了,为什么还要再获取一次呢, 是不是解决递归调用一个带锁
  的方法,再释放锁前再次调用,就是再次获取一次了。
独占锁:一旦一个线程获取了这个锁,再未释放前,其他线程将无法获取该锁。

ReentrantLock 又分成公平锁和非公平锁(默认)。
  公平锁:多个线程都在等待着同一把锁,锁的占有者释放锁时,谁等待的时间最长,
    谁能获取该锁。
  
  非公平锁:一个线程释放锁以后,其他等待的线程一哄而上,但是只有一个能够成功获取。
  
锁的获取方式:四种:
  lock.lock();                  阻塞式获取锁
  lock.lockInterruptibly();     阻塞式获取锁,可以被打断
  lock.tryLock()                非阻塞式获取锁,返回 true 表示获取了锁
  lock.tryLock(timeOut, unit)   带超时的阻塞式获取锁。
  
  他们的实际上的区别还需要看源码才能明白
  
锁的释放:
  lock.unlock();   一般要放到 finally 中执行,释放锁是一定要做的。
  获取一次锁,就要释放一次, 二者要对应
  
条件【Condition】:
  锁上还可以带条件: Condition cd = lock.newCondition();
  条件的使用也应该放到 lock()  和 unlock() 之间。 使用生产者和消费者模型来说。
  
  生产者获取了锁,开始生产,发现仓库已满,不能再生产了, 于是就调用【生产条件】await,
  await 的调用让生产者线程放弃了锁, 并且阻塞在 await 上。
  
  生产者放弃了锁,此时消费者就能获取这个锁。于是便来消费。消费者消费一个后,仓库
  就空了一个, 此时可以通知生产者继续生产的。 需要注意的是,此时消费者还是持有锁的。
  生产者现在虽然被通知可以继续生产,但是消费者此时并没有释放锁。 因此需要消费者
  把锁释放以后,生产者才有可能获取锁。 只有生产者获取锁以后,才能从 await 的阻塞
  状态中返回。 然后继续执行 await 后的代码

 private ReentrantLock lock = new ReentrantLock();
 private Condition cp = lock.newCondition();
 private Condition cc = lock.newCondition();
 
  生产者                        消费者
 循环执行                      循环执行
  lock                          lock
    |                             |
   满了-Y- cp.await             空了-Y- cc.await
    |N      /                     |N      /
   继续生产                      继续消费
    |                             |
  通知消费 cc.signal            通知生产 cp.signal
    |                             |
   unlock                        unlock
   
 按照上面说的,生产者阻塞再 cp.await 上, 消费者 cp.signal 通知了生产者。
 但是只要 消费者还没有 unlock, 生产者仍然无法运行。 直到消费者 unloack,
 生产者才有可能 获取锁,并执行 cp.await 下面的代码。消费者 unlock 后,生产者不是
 从头开始执行 lock, 而是直接从 cp.await 中唤醒,并执行后面的代码
 cp.await 的作用是:阻塞当前线程,并且释放持有的锁
 被别的线程 signal 后,阻塞再 cp.await 的线程被唤醒,但是要先获取锁,然后向下执行。

到现在关于表面上的认知已经阐述完了。具体看源码:
================================================================================
两个线程争抢一个锁, 抢到锁的线程,会再次获取一次锁(体现可重入), 然后释放锁(2次)
第一个抢到锁的线程释放锁以后,阻塞再 lock 上的线程才有机会获取锁。
private void t1()
{
    ReentrantLock lock = new ReentrantLock();
    Runnable r = () -> {
        lock.lock();
        try {
            System.out.println("已获取锁:" + Thread.currentThread().getName());
            sleep((int) (Math.random() * 4000 + 1000));

            lock.lock();
            try {
                System.out.println("已获取锁2:" + Thread.currentThread().getName());
            }finally {
                System.out.println("释放锁2:" + Thread.currentThread().getName());
                lock.unlock();
            }
        } finally {
            System.out.println("释放锁:" + Thread.currentThread().getName());
            lock.unlock();
        }
    };

    new Thread(r, "[线程1]").start();
    new Thread(r, "[线程2]").start();
}

输出:
已获取锁:[线程2]
已获取锁2:[线程2]
释放锁2:[线程2]
释放锁:[线程2]
已获取锁:[线程1]
已获取锁2:[线程1]
释放锁2:[线程1]
释放锁:[线程1]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
ReentrantLock 类中有三个 内部类,
abstract static class Sync extends AbstractQueuedSynchronizer
static final class NonfairSync extends Sync
static final class FairSync extends Sync

Sync 是继承前面分析的 AQS 。 
这里要使用 AQS 的 独占模式, CountDownLatch 使用了 AQS 的共享模式。

ReentrantLock 有两个构造方法,用于构建 公平锁 和 非公平锁(默认)

public ReentrantLock() {
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
非公平锁情况:
lock.lock();

    public void lock() {
        sync.lock();
    }
    
    //NonfairSync::lock
    final void lock() {
        //进来就想要改 AQS 的 state, 从 0 改成1.
        //改成功:说明当前锁还还没有人持有,现在自己持有了
        //改失败:1、 说明当前锁已经有人持有, 
                  2、 当前并没有人持有锁,但多线程正在抢锁,自己竞争失败了
                  3、 自己就是持有锁的线程,又来获取一次(重入)
        if (compareAndSetState(0, 1))
            //自己获得了锁,把 AQS 中持有锁的线程更新为本线程。 获取锁倒是简单
            setExclusiveOwnerThread(Thread.currentThread());
        else
            //锁早就被人持有,或者,上面竞争失败了,或者自己至少重入而已
            acquire(1);
    }
    
    //AQS::acquire
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&               //-------------1
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//-------------2
            selfInterrupt();                  //-------------3
    }
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-------------1

    //NonfairSync::tryAcquire
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
    
    //Sync::nonfairTryAcquire
    final boolean nonfairTryAcquire(int acquires) {
        //来到这里说明,自己没有获取锁,或者自己就是锁的持有者,但需要重入锁
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            //上面没有获取锁,此时运行到这里,可能有人已经释放锁了,再试一次
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            // 本线程是锁持有值,就是想要重入锁
            // 重入锁就是把状态加1, 那么unlock 应该就是-1了, 
            // 所以获取和释放要一一对应
            int nextc = c + acquires;   
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        
        //没能获取到锁的线程。
        return false;
    }

所以 ----1 方法返回后, 持有锁的线程返回 true, 结束后续的代码执行
返回 false, 表示 没有获取到锁的线程, 需要继续执行后续代码。

-------------2
没有获取锁的线程,继续执行:

addWaiter(Node.EXCLUSIVE), arg) : 
前面介绍过,创建一个线程专属的 node 对象,并且把 node 添加到第一个节点是 head 的
双向链表中,并且返回这个 node 对象。注意这里的模式是 :Node.EXCLUSIVE

// AQS::acquireQueued
// 这段代码看着好眼熟啊, 可以对比一下: AQS::doAcquireSharedInterruptibly(arg)
final boolean acquireQueued(final Node node, int arg) {
    // 进入这里的线程都是没有获取锁的线程,
    // 获取锁的那个线程,或者重入锁的情况,是不可能执行到这里的。
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            // 本线程没有获取锁,自己的前置节点就是 head, 
            // 又过了这么久使用 tryAcquire(arg) 再试一次,万一获取锁了呢
            if (p == head && tryAcquire(arg)) {
                //万一的情况发送了,还真的获取了锁。 那就把自己设置成 head
                setHead(node);
                p.next = null; // help GC
                failed = false;
                
                //返回 是否被打断, 但是 finally 中代码不执行
                return interrupted;
            }
            /*
             * 自己不是 head 的后一个节点,或者是 head 的后一个,
             * 但是再次尝试获取锁还是不成功。
             * shouldParkAfterFailedAcquire : 找到一个仍然有效的前节点,把它的
             *   状态设置成功 SIGNAl, 让前节点有义务唤醒本线程。 如果设置不成功,
             *   就不能休眠,再循环一次,一定要设置成功
             * parkAndCheckInterrupt()  设置前置节点的 状态为 SIGNAl 成功,
             *   本线程可以安心地无限期休眠了。
             */
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}


-------------3: 
正常流程是: 没有获取锁的线程,就阻塞在 ----2 里面, 所以 ----3 没有机会执行。
那就看看正常流程下,一个线程释放锁,有干了什么
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
本篇完

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值