多线程之线程中断

lock锁中有一段代码:

        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                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;
        }
    }

这段代码表示了多线程在竞争锁的逻辑。

其中,对于

else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }

这段代码来说,为什么没有加上同步机制?那是因为AQS提供了获取得到当前锁的线程,以及同步器状态的记录,通过这两个成员,可以知道是锁正在被哪个线程所持有。这里可以通过一个图来进行说明

而至于为何又会有nextC,那是因为可能存在着某种特殊的场景:

public void A(){
	lock.lock();
    	xxxxx;
    	B();
    lock.unlock();    
}

public void B(){
    lock.lock();
    lock.unlock();
}

也就是说在执行A的之后,又会调用方法B来进行执行,那么对于此时,对于同一个线程来说,就会两次持有锁,那么这就是上面的nextc相加的原因了。

同理可以推断,syncronized也是可重入的

排队方法:

    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

从这里可以看到,造就了一个CLH队列。Node节点中保存了对当前线程的引用

对于Node节点中的几个重要属性:

pre,next,waitstatus,thread,mode

首先来说一下:

mode表示的有两种形式:一种是独占也就是互斥,另外一个是共享;

waitstatus代表着当前节点的生命状态,有五种生命状态,分别代表着不同的情况

    private Node enq(final Node node) {
        // 所有的线程都得进来进行排队,不允许漏掉任何一个
        // 不然底层JVM创建好的线程栈等等信息就会造成内存泄漏等情况
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

这里有一段代码需要来进行说明:

// 看下具体的说明方式
compareAndSetTail(t, node)
    /**
     * CAS tail field. Used only by enq.
     */
    private final boolean compareAndSetTail(Node expect, Node update) {
        // tailOffset代表的是这个变量在当前对象中的偏移量
        return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
    }

也就是说将当前的tail指向改变,改变指向队尾的最后一个元素。

而原来的变量t却在此时不受任何影响,直接返回的就是变量t,也就是插入结点的前一个结点。

acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

这个方法转化一下:

// 当前进来的线程节点
acquireQueued(node, arg))

此时此刻代表的是进来排队的线程排好队之后,需要来进行阻塞了。

    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;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

在第一轮循环中,只是修改了前驱节点的状态,也仅仅只是做了一个修改而已,第二个节点及其后续的没有法神改变。表示的首结点可以被唤醒;第二轮循环才真正的表示的是可以被其进行阻塞,也就是把所有的线程都给阻塞掉了。

同时判断线程是否会有中断线程来进行唤醒的?

ReetrantLock是一个可以中断的锁

shouldParkAfterFailedAcquire(p, node)

在这个方法中,首先会将node的节点,也就是队首的节点的waitstatus从0->-1,这样做是因为想要在公平锁的情况下,当线程占有的锁被释放后,需要判断队首的节点的状态是否是-1,如果是-1,表示的可以被唤醒。

然后被唤醒后会再走一轮循环,然后拿到锁之后了再去进行执行。

但是这里可能存在着非公平的情况下,如果新来的线程和队首的线程同时来进行抢,那么这个时候会需要问题,位于队首的节点可能会再次进入到阻塞状态,那么再次把0修改成-1,放入到队列中来进行排列。再此经历过两轮循环,才能够重新恢复到-1状态

综合情况就是:

1、线程之间开始抢占同步器锁;

2、没有抢到的线程开始进行排队;

3、排完队之后开始进行阻塞,当然并非是上来就开始进行阻塞,队列首结点需要再次来进行试探;

4、执行完成之后开始进行唤醒;

posted @ 2021-10-14 11:34  雩娄的木子  阅读(81)  评论(0编辑  收藏  举报