JUC--008--locks4

上一篇:
获取到锁的线程,在执行自己的代码, 没有获取锁的线程在使用 lock.lock() 获取锁
的时候阻塞了。
此时按照正常流程,获取锁的线程释放锁,看看会发生什么。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
lock.unlock();

    public void unlock() {
        sync.release(1);
    }
    
    //AQS::release
    public final boolean release(int arg) {
        // tryRelease 把 状态 state 减去 arg 。
        // 因为可重入,所以未必能够减成 0,  
        // 返回 true 表示,减成0了,锁释放
        // 返回 false 表示,只是线程重入多次,释放一次而已
        if (tryRelease(arg)) {
            //进入:锁要释放了。
            Node h = head;
            if (h != null && h.waitStatus != 0)
                /*
                 * unparkSuccessor 这个方法以前看过啊,
                 * 让方法的参数的节点的下一个节点解除阻塞。
                 * 如果下一个节点为空,或者下一个节点被取消,那就从 tail 
                 * 开始向前找,找到离参数后面的最近的一个有效的节点,解除他的阻塞。
                 */
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    
    //Sync::tryRelease
    protected final boolean tryRelease(int releases) {
        int c = getState() - releases;
        /* 当前线程不是持有锁的线程,释放锁,抛异常, 所以:
         * ReentrantLock lock = new ReentrantLock();
         * lock.unlock(); //抛异常
         * 一个线程调用 lock.unlock(); 前必须获取了锁
         */
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        
        //因为可能是被线程重入了多次, unlock 一次,减一次
        if (c == 0) {
            //终于把 state 减成了 0, 把锁的持有者清空
            free = true;
            setExclusiveOwnerThread(null);
        }
        setState(c);
        return free;
    }
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
线程中调用 lock.unlock(),  最后执行了 unparkSuccessor(h);
解除了 head 后面的一个节点的阻塞状态。 所以现在有线程已经被唤醒。
到现在就有两个线程:一个是:刚刚释放锁的线程,他一直都没有阻塞, 另一个是刚唤醒的
前面的示例是一个线程抢一次(获取在重入一次)然后释放就退出了, 如果放到循环中, 
就会出现自己刚释放自己又去抢的情况。
for(;;) {
  lock.lock()
  ...
  lock.unlock()
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//AQS::acquireQueued
//这个方法又来了,前面说过,多个执行 lock.lock() 的线程阻塞在 ----A 上。
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) { //--------------B
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())      //--------------A
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
--------------A:
一个线程被唤醒, 而且是正常唤醒, 所以 parkAndCheckInterrupt() 返回 false 表示
不是被线程打断唤醒的。所以 ----A 处的 if 进不去。然后就来了一次循环。

--------------B
被唤醒的就是 head 的后一个节点的线程,所以此时 p 就是 head
而 tryAcquire(arg) 就真的能够返回 true 表示加锁成功吗? 未必,因为现在还是有两个
线程在争抢:(刚释放锁的线程 和 刚唤醒的线程即本线程)
  只不过刚释放锁的线程再抢锁是重新调用 lock.lock
  而唤醒线程枪锁的是在 ------B: tryAcquire(arg) 中进行的。
  
  如果是释放锁的线程又抢到锁, 被唤醒线程在 ----B: tryAcquire(arg)  返回false
  再次来到 -----A 中,又被阻塞
  
  如果是唤醒线程抢到锁, 那么原来释放锁的线程通过调用 lock.lock, 一路执行,也来到
  本方法,创建了自己的节点,把自己的节点添加到双向链表上, 然后也阻塞在 ----A
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
此时完成 获取锁,阻塞线程,释放锁,再获取,再阻塞 .... 的流程。
认知也发生了一些改变,前面提到,持有非公平锁的线程释放锁后, 那些因为没有获取锁
而阻塞的线程会一哄而上争抢锁。 看完源码后知道了,并不是所有被阻塞的线程都去争抢
锁了。释放锁后,只会唤醒一个线程去抢锁。他的竞争对手是 刚刚释放锁的线程,当然
还可能是全新的线程也第一次想通过 lock.lock 竞争锁。

唤醒的线程是 head 的后一个,或者head后,离head最近的,有效的一个节点。虽然是
非公平锁, 但还是有一点有序。 对于同在阻塞链表中的节点来说,是公平的, 阻塞添加
时都是添加到后面,唤醒都是先唤醒前面的,也体现了先来后到。
================================================================================
先看一个的现象:

private static void t2()
{
    new Thread(()->{
        Thread.currentThread().interrupt(); //中断自己先

        System.out.println("中断1:" + Thread.currentThread().isInterrupted());
        System.out.println("中断2:" + Thread.currentThread().isInterrupted());

        MyUtils.sleep(1000);

        System.out.println("中断3:" + Thread.currentThread().isInterrupted());

        MyUtils.sleep(1000);

        System.out.println("中断4:" + Thread.currentThread().isInterrupted());
        System.out.println("执行完成");
    }).start();
}

输出:
中断1:true
中断2:true
java.lang.InterruptedException: sleep interrupted ....
中断3:false
中断4:false
执行完成

线程开始并没有阻塞,但是上来就给自己一个中断。
中断1 为 true, 好理解, 但是 中断2 怎么还是 true ,不是说调用两次就能清除中断状态吗?

看一下源码:
内部会调用下面 native 方法,传递了 false。 传递 false 表示并不会擦除中断的状态
public boolean isInterrupted() {
    return isInterrupted(false);
}
private native boolean isInterrupted(boolean ClearInterrupted);

所有 中断2 还是 true 。
接着调用 sleep(1000) 。 因为当前线程时中断状态,所有,sleep 立刻返回,而且
抛出异常。 接下来中断状态变成 false, 感觉是被 sleep 中和了。
后面 在 sleep 就能正常sleep 了。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
private static void t2()
{
    new Thread(()->{
        Thread.currentThread().interrupt(); //中断自己先

        System.out.println("中断1:" + Thread.interrupted());
        System.out.println("中断2:" + Thread.currentThread().isInterrupted());

        MyUtils.sleep(1000);

        System.out.println("中断3:" + Thread.currentThread().isInterrupted());

        MyUtils.sleep(1000);

        System.out.println("中断4:" + Thread.currentThread().isInterrupted());
        System.out.println("执行完成");
    }).start();
}
    
输出:
中断1:true
中断2:false
中断3:false
中断4:false
执行完成

这里的 sleep 都是正常的,没有异常出现, 唯一的不同点在 中断1 后面的方法

//成员方法
public boolean isInterrupted() {
    return isInterrupted(false);
}

//静态的
public static boolean interrupted() {
    return currentThread().isInterrupted(true);
}

他们都调用了 native 方法。只是传递的参数不同。
传递 true 表明清除中断标记
传递 false 表明,不会清除中断标记。
private native boolean isInterrupted(boolean ClearInterrupted);

上面 isInterrupted 和 interrupted 两个方法时获取状态,以及是否擦除中断状态
而下面这个方法时行动。 中断线程,并设置为中断状态。 如果当前没有阻塞,先调用
这个方法,给线程设置了中断的状态,需要小心了,那些阻塞方法(sleep, wait, 
countdownLatch.await阻塞等)遇到中断状态,会抛出异常立刻返回,
并把状态设置为 false

//成员方法:中断线程,把中断设置成 true
public void interrupt() {
    if (this != Thread.currentThread())
        checkAccess();

    synchronized (blockerLock) {
        Interruptible b = blocker;
        if (b != null) {
            interrupt0();           // Just to set the interrupt flag
            b.interrupt(this);
            return;
        }
    }
    interrupt0();
}

有了这些说明以后,再来看看,如果 lock.lock() 阻塞被 线程中断会怎么样。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
private void t1() throws InterruptedException {
    ReentrantLock lock = new ReentrantLock();
    Runnable r = () -> {
        lock.lock();
        try {
            System.out.println("已获取锁:" + Thread.currentThread().getName());
            sleep(10000);
        } finally {
            System.out.println("释放锁:" + Thread.currentThread().getName());
            lock.unlock();
        }
    };

    Runnable rBlock = () -> {
        sleep(1000);
        lock.lock();
        try {
            System.out.println("已获取锁:" + Thread.currentThread().getName());
            System.out.println(Thread.currentThread().isInterrupted());

            //可以使用 Thread.interrupted() 清除中断状态,否则 sleep 会抛出异常。
            //System.out.println(Thread.interrupted());

            sleep(10000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        finally {
            System.out.println("释放锁:" + Thread.currentThread().getName());
            lock.unlock();
        }
    };

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

    t1.start();
    t2.start();

    new Thread(()->{
        sleep(5000);
        t1.interrupt();
        System.out.println("已经打断线程1");
    }, "[线程3]").start();
}

线程1先 sleep 1秒, 确保 线程2 能够得到锁。 线程2 获取锁并持有10秒, 线程1阻塞
在 lock() 上。5秒后,线程3中断线程1。 线程1 是否能够从 lock() 中解除阻塞并向下
运行呢? 是不可以的, 所有 lock.lock() 是不可被中断的 锁。lock.lock() 只能是
通过竞争,获取锁,才能解除阻塞。 10000 秒后 线程2 释放了锁。此时线程1 获取了锁
可是执行到 sleep 时, 立刻抛出 interrupted 异常。 接着释放锁,结束了。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
为什么会这样,继续看源码:

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();                  //---------------------C
    }
    
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;        //---------------------B
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())    //---------------------A
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

线程2,获取了锁, 导致线程1 阻塞在  ----A 上, 此时线程1 被中断, 从 ----A 中返回。
并且是因为 中断返回的, 所以 ---A 处的 if 判断成立, interrupted 变量被赋值为 true.
接着下一次循环, 此时 线程2还在持有锁, 所以 线程1 又阻塞在 ---A 上。因此,
线程中断并不能让阻塞解除。 直到 线程2 释放了锁, 此时 线程1 再次被唤醒,从 ---A
中返回,然后获取锁成功, 代码从 ---B 处退出, 并返回 interrupted(此时为 true)

代码执行回到了 ---C: selfInterrupt() { Thread.currentThread().interrupt();}
这里又调用了一次,让线程中断,把线程的中断状态设置成 true.

所以运行到用户代码时,遇到 sleep 这样的阻塞方法,不会阻塞,而抛出异常。
所以写这段代码的时候一定要注意,自己的线程是否被中断过。 如果被中断过, 那么调用
一些阻塞的代码的是否可能会抛出异常。超出我们的预期。 所以如果想要擦除异常,使用
Thread.interrupted() 可以擦除线程的中断状态。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
到现在 lock.lock() 获取锁的方式就说完了,总结一下:
lock.lock() 是不可被线程中断的异常,没有获取到锁的线程在 一个双向链表中排队。
并被 unsafe.park 阻塞线程。 当锁释放, 总是链表的第一个 和 释放锁的线程 还有
新的线程 抢占锁。链表第一个线程如果没有抢到,继续阻塞, 如果其他两个没有抢到,
就会被加入到链表的尾部。 特别注意的是,如果线程曾经被打断,需要小心,下一次
的阻塞代码可能抛出异常。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
如果是公平锁,使用 lock.lock() 又是怎么样的公平呢
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
公平锁:
    final void lock() {
        acquire(1);
    }
    
非公平锁:
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }
    
二者都调用的方法: acquire()

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

最后一处的差别:tryAcquire(arg) 被复写了, 实现不同:

公平锁:
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
        
            //和 非公平锁的区别就是 多判断异常 hasQueuedPredecessors()
            
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                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;
    }
        
非公平锁:
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }

    final boolean nonfairTryAcquire(int acquires) {
        和公平锁 tryAcquire 的实现就是少了 【!hasQueuedPredecessors() &&】 这个判断
    }
    
hasQueuedPredecessors() 自己不是双向链表 head 的后面一个【感觉这个方法的命名
和文档描述并没有体现出方法的意图,甚至文档 中返回值的描述都是误导性的,
可能是文档描述的节点是不包含head的? 所以head后的节点是没有前节点的。
但是对于一个还未加入的线程来说,自己也应该是没有前节点的才对,反正现在很乱】
那些没有获取锁的线程都按照顺序被放到双向链表中。 此时持有锁的线程释放了锁。
按照非公平的锁,现在又三股势力在抢锁。1、阻塞链表中 head后节点(此时被唤醒),
2、刚刚释放锁的线程, 3、新的线程,第一次来抢。  非公平锁时,他们都有可能抢到。

如果时公平锁呢?抢锁的还是他们三个(这和抢不抢没有关系,因为他们线程没有阻塞,
正常运行而已)。 但是只能是 双向链表的 head 后置节点能抢到。原因就是这个方法:
hasQueuedPredecessors()。 释放锁的线程 和 新线程,他们现在都还不在双向链表中。
所以他们调用这个方法返回了 true,  结果外面取反,变成 false。 他们只能加入到链表。

所以公平和非公平的区别就是:释放锁以后,是不是剥夺了 释放锁的线程 和 新线程的
抢锁资格。共同点就是 双向链表中的众多节点,只能由 head 后面的那个节点去参与抢锁。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~



 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值