其他分享
首页 > 其他分享> > JUC--009--locks5

JUC--009--locks5

作者:互联网

前面已经说了 ReentrantLock 使用 lock.lock() 方式获取锁,而且测试过这中获取锁的方式
如果获取不到锁就会阻塞,而且无法通过线程中断的方式解除阻塞。如果使用线程中断
试图解除阻塞,虽然不会成功,但是会把线程的状态改成已中断,这个可能会影响后续代码。
同时也说了公平锁和非公平锁的区别。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
获取锁的四种方式:
  lock.lock();                  阻塞式获取锁
  lock.lockInterruptibly();     阻塞式获取锁,可以被中断
  lock.tryLock()                非阻塞式获取锁,返回 true 表示获取了锁
  lock.tryLock(timeOut, unit)   带超时的阻塞式获取锁。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
现在看一下 lock.lockInterruptibly();  如何做到可以被中断的。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
示例代码:
开启两个子线程:线程1 进来先sleep(1000), 确保 线程2 能够获取锁。线程2 获取锁以后
准备长时间持有。 所以 线程1 阻塞在 lock.lockInterruptibly() 上。 但是主线程 5秒后
让 线程1 中断。线程1 中断后,从 阻塞中抛出异常。进入 finally 中执行。 这时一定
要判断一下,当前线程是否持有锁。已经看过释放锁的代码,没有锁的线程试图释放锁会
抛出异常。


private void t1() throws InterruptedException {
    ReentrantLock lock = new ReentrantLock();

    Runnable r = () ->
    {
        try {
            lock.lock();
            System.out.println("已获取锁:" + Thread.currentThread().getName());
            sleep(1000000);
        } finally {
            lock.unlock();
        }
    };

    Runnable rBlock = () ->
    {
        sleep(1000);
        try {
            lock.lockInterruptibly();
            System.out.println("--已获取锁:" + Thread.currentThread().getName());
        } catch (Exception e) {
            e.printStackTrace();
        }
        finally {
            if(lock.isHeldByCurrentThread()) {
                lock.unlock();
            } else {
                System.out.println("--未曾拥有:" + Thread.currentThread().isInterrupted());
            }
        }
    };

    Thread t1 = new Thread(rBlock, "[线程1]");
    Thread t2 = new Thread(r, "[线程2]");

    t1.start();
    t2.start();
    sleep(5000);
    t1.interrupt();
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
lock.lockInterruptibly();

    //ReentrantLock::lockInterruptibly
    //调用的方法是不同的:lock.lock() 调用的是 acquire(1);
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }
    
    //AQS::acquireInterruptibly
    /*
     * 如果线程已经存在中断状态,下面进入第一个 if 直接抛出异常。
     * lock.lock()               -----1
     * lock.lockInterruptibly()  -----2
     * 假设我们想通过 1 获取锁,然后使用 2 重入一次。结果在 1 中阻塞。期间被中断
     * 只是 lock.lock 不能中断,所以没有什么表现,结果等到 1 获取了锁,然后
     * 使用 2 重入一次的时候,却有表现了,直接抛出异常。这个可能会出乎意料。
     * 
     * 前面已经多次提到过这个,这里最后一次重复了, 想要不抛出异常,清除线程中断状态。
     */
    public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        //此时再尝试一次是不是能够获取锁
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }
    
    
    //AQS::doAcquireInterruptibly
    //这个代码看着好眼熟啊, CountDownLatch::await 的调用代码和这个很像,
    //即这个方法: doAcquireSharedInterruptibly, 只他是共享的,这里的这个是独占的
    /*
     * 这个方法就不细说了,创建一个属于当前线程的节点,添加到双向链表,
     * 把自己前面的节点的 状态设置成 SIGNAL, 然后自己通过 unsafe.park 阻塞
     * 
     * 情况1: 自己线程并没有被中断,而是有人释放了锁,如果本线程Node就是 head 后面
     *        的那一个,结果被唤醒,去争抢锁,如果失败了在这里循环一次,继续阻塞。
     *        如果成功抢到锁,从 --OUT 处出去, finally 的代码条件不满足,不执行
     * 情况2:还在阻塞的时,线程被中断,从 --wake 处返回 true, 进入 if 语句,抛出异常
     *        执行 finally 中的代码(条件满足了), finally 中的代码就是 把自己的节点
     *        删除,如果自己还有后续节点,也唤醒自己的下一个节点。 然后返回到用户代码
     *        这里抛出的异常没有被捕获,直接抛给用户代码。
     *
     */
    private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return;       //----------OUT
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())    //----------wake
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

关于 lock.lockInterruptibly() 的代码执行流程就结束了。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
第三种获取锁的方式: lock.tryLock()
tryLock() 不会阻塞,而是立刻返回。根据返回值判断是否获取了锁。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
使用示例:

如果没有获取锁,那就去干点别的事情,然后循环的使用 tryLock() 尝试获取锁。
获取锁以后执行任务,最后还有释放锁

private void t1() throws InterruptedException {
    ReentrantLock lock = new ReentrantLock();

    Runnable r = () ->
    {
        try {
            lock.lock();
            System.out.println("已获取锁:" + Thread.currentThread().getName());
            sleep(5000);
        } finally {
            lock.unlock();
        }
    };

    Runnable rBlock = () ->
    {
        sleep(100); //确保线程2获取锁
        while(true) {
            if(lock.tryLock()) {
                System.out.println("已获取锁:" + Thread.currentThread().getName());
                try {
                    System.out.println("执行任务");
                    sleep(1000);
                    System.out.println("执行任务--完成,准备退出");
                    break;
                } finally {
                    System.out.println("释放锁");
                    lock.unlock();
                }
            } else {
                System.out.println("没有获取锁,干点别的");
                sleep(1000);
            }
        }
        System.out.println("线程退出");
    };

    Thread t1 = new Thread(rBlock, "[线程1]");
    Thread t2 = new Thread(r, "[线程2]");

    t1.start();
    t2.start();
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
tryLock() 方法的 java doc 上介绍,这个方法时无视公平原则的。即使时公平锁,也有
很多的线程再排队等待获取锁,但是这个方法,如果锁可用,他就会去抢。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
lock.tryLock()

    public boolean tryLock() {
        return sync.nonfairTryAcquire(1);   //难怪他会无视公平原则。
    }
    
    //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()) {
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
    
没了,代码就这么多,使用这种方式获取锁, 要么获取到,要么获取不到,但自己不会
像前面的那些获取锁的方式,如果获取不到就创建一个Node节点, 添加到双向链表,并委
托自己的前置节点将自己唤醒。tryLock() 来去自由,感觉还洒脱。所以他也不存在中断,
排队等麻烦事。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
lock.tryLock(timeOut, unit): 带超时的阻塞式获取锁方式。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~、、
使用代码:
tryLock(timeOut, unit) 带超时的获取锁。
获取锁立刻返回, 否则阻塞指定的时长,自动被唤醒,再次尝试获取锁。
阻塞时,如果自己被唤醒,也会参与抢锁行动,阻塞时也能被中断,所以会抛出中断异常。

使用代码意图:
先确保 线程2 获取锁,并持有锁10后释放。 线程1 开始带超时的阻塞 3 秒,但是 2 秒后,
主线程让 线程1 中断, 所以线程1抛出异常,然后异常被捕获,继续循环,再次带超时的
阻塞 3 秒。 此时线程2 仍然没有释放锁,超时时间到后,自动解除阻塞,返回 false,去
做了其他事情,然后再次循环, 再次尝试获取锁,又带超时的开始阻塞 ....
线程2 持有锁10秒后 释放了锁, 线程1立刻解除阻塞,开始执行代码。
执行完成后 break 退出,并释放锁。


private void t1() throws InterruptedException {
    ReentrantLock lock = new ReentrantLock();

    Runnable r = () ->
    {
        try {
            lock.lock();
            System.out.println("已获取锁:" + Thread.currentThread().getName());
            sleep(10000);
        } finally {
            lock.unlock();
        }
    };

    Runnable rBlock = () ->
    {
        sleep(100); //确保线程2获取锁
        while(true)
        {
            try {
                System.out.println("准备获取锁:" + Thread.currentThread().isInterrupted());
                if(lock.tryLock(3, TimeUnit.SECONDS))
                {
                    System.out.println("已获取锁:" + Thread.currentThread().getName());
                    try {
                        System.out.println("执行任务");
                        sleep(1000);
                        System.out.println("执行任务--完成,准备退出");
                        break;
                    } finally {
                        System.out.println("释放锁");
                        lock.unlock();
                    }
                } else {
                    System.out.println("没有获取锁,干点别的");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("线程退出");
    };

    Thread t1 = new Thread(rBlock, "[线程1]");
    Thread t2 = new Thread(r, "[线程2]");

    t1.start();
    t2.start();
    sleep(2000);
    t1.interrupt();
}

输出:
已获取锁:[线程2]
准备获取锁:false
java.lang.InterruptedException .....
准备获取锁:false
没有获取锁,干点别的
准备获取锁:false
没有获取锁,干点别的
准备获取锁:false
已获取锁:[线程1]
执行任务
执行任务--完成,准备退出
释放锁
线程退出
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
tryLock(long timeout, TimeUnit unit) 
java doc 上介绍,如果是公平锁,那么他会遵守公平规则。 但是可以这样
if(lock.tryLock() || lock.tryLock(timeout, unit)) { ... } 先无视规则的
获取一次,只有获取不到锁,然后再来遵守规则。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
lock.tryLock(3, TimeUnit.SECONDS)


    //Sync::tryLock
    public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }
    
    //AQS::tryAcquireNanos
    public final boolean tryAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        //如果线程已经又中断状态,直接抛出异常
        if (Thread.interrupted())
            throw new InterruptedException();
        /*
         * tryAcquire(arg) 的实现,公平锁和非公平锁是有区别的。
         * 所以 tryLock(timeout, unit) 非遵守公平锁的原则。
         *
         * tryLock(timeout, unit) 再次尝试获取锁,如果获取不到,就继续。
         */
        return tryAcquire(arg) ||
            doAcquireNanos(arg, nanosTimeout);
    }
    
    //AQS::doAcquireNanos
    //这个代码看着也熟悉,CountDownLatch.await(xx, xx) 调用的方法和
    //这个很像,那个是共享模式, 这里的是独占模式。
    private boolean doAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        /*
         * 如果 tryLock(小于等于0的数),  如果执行到这里之前能够获取锁,就获取了。
         * 如果不能获取锁,来到这里,自己不会被加到队列,不会被阻塞,而是立刻返回。
         * 
         * 这样的话,我们就能实现一个遵守公平规则的 tryLock(), 即
         * tryLock(0, unit) 。 按照公平规则获取锁,获取不到也不阻塞
         */
        if (nanosTimeout <= 0L)
            return false;
            
        //未来到期的相对时间
        final long deadline = System.nanoTime() + nanosTimeout;
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                //自己不是被打断方式唤醒(即正常唤醒),或者自己还没有被阻塞过。
                //如果自己是 head 的后一个, 参与抢锁行动
                if (p == head && tryAcquire(arg)) {
                    //抢到锁, 把head干掉,自己变成新 head
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    
                    //获取锁,正常退出, finally 中的不用执行了。
                    //因为 head 被干掉,自己变成 head, 所以相当于把自己删除了,
                    //而且自己获取了锁,也没有必要唤醒下一个起来尝试抢一下锁。
                    return true;  
                }
                
                //离到期(超时)剩余的时间
                nanosTimeout = deadline - System.nanoTime();
                
                /*
                 * 这里的 if 内部是一个出口:
                 * 情况1:第一次进来,把自己的 node 刚添加进去,结果时间到到期了。 
                 *        此时自己没有获取锁,而且 finally 种的条件满足,会把自己
                 *        刚刚添加进来的 node 删除掉
                 * 情况2:像情况1一样,自己是第一次进来,虽然第一次循环这里不满足,
                 *        但是自己到期时间已经小于 spinForTimeoutThreshold(1000纳秒)
                 *        此时如果让线程阻塞不太划算, 1000纳秒太短,刚阻塞就要解开。
                 *        因此就不阻塞了, 在这个循环中转2圈,时间就耗掉了。所以
                 *        情况2就是没有阻塞,在这转圈耗时间,待满足退出,执行 finally
                 * 情况3:到期时间还有点长,阻塞了,然后超时后自动解除阻塞,再来一次
                 *        循环,这里就满足了,返回 false, 执行 finally
                 * 不管哪种情况,如果代码从这个出口出去的,就表示并没有获取锁,
                 * 而且执行 finally, 把自己的 node 从 双向链表中删除,如果有需要还要
                 * 唤醒自己的下一个节点。
                 */
                if (nanosTimeout <= 0L)
                    return false;
                
                //把自己的前置节点的状态设置成 SIGNAL
                if (shouldParkAfterFailedAcquire(p, node) &&
                    //剩余时间还很长,大于这个常数 1000 纳秒
                    nanosTimeout > spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanosTimeout); //待超时阻塞
                
                //超时前,自己被中断,从阻塞中返回,抛出异常
                //执行 finally , 清除自己的 node, 如果有需要,还要唤醒下一个节点
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                //从双向链表中清除参数 node , 如果有需要同时唤醒 node 的下一个节点
                cancelAcquire(node);
        }
    }
    
根据上面的源码: 如果自己的线程被打断,那么抛出的异常没有被捕获,会抛给用户代码。
而且 在抛出异常前使用  Thread.interrupted() 获取线程是否被打断,这个方法会
清除线程的中断状态的, 所以用户代码会收到一个异常,但是获取中断状态是 false, 
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
到这里四种获取锁的方式说完了0

    方式                |   阻塞       |  可否打断    |   遵守公平规则
------------------------+--------------+--------------+-----------------
lock.lock()             |   永久阻塞   |    不可      |    遵守
------------------------+--------------+--------------+-----------------
lock.lockInterruptibly()|   永久阻塞   |    可        |    遵守
------------------------+--------------+--------------+-----------------
lock.tryLock()          |   非阻塞     |    ---       |    不遵守
------------------------+--------------+--------------+-----------------
lock.tryLock(x, unit)   |   超时阻塞   |    可        |    遵守





 

标签:JUC,Thread,--,lock,阻塞,获取,线程,tryLock,009
来源: https://blog.csdn.net/szw727/article/details/114371795