5.加锁&解锁&公平&非公平&打断&不可打断原理

211 阅读10分钟

非公平锁加锁线程1流程

1.cas设置sate从0变为1

2.设置独占线程为当前线程

3.注意线程1不需要入队

public void lock() {
    sync.lock();
}
//每个线程调用lock方法都会尝试获取一次锁
final void lock() {
    //线程1到这里就结束了 获取锁成功
    //首先用 cas 尝试(仅尝试一次)将 state 从 0 改为 1, 如果成功表示获得了锁
    if (compareAndSetState(0, 1))
        //设置独占线程为自己
        setExclusiveOwnerThread(Thread.currentThread());
    else
        //线程2走这里
        acquire(1);
}
protected final void setExclusiveOwnerThread(Thread thread) {
    exclusiveOwnerThread = thread;
}

非公平锁加锁线程2流程

1.判断加锁状态和锁重入

2.enq尾部入队(注意head是个哑结点)

3.进入同步阻塞队列

waitStatus负值表示结点处于有效等待状态,而正值表示结点已被取消。

所以源码中很多地方用>0、<0来判断结点的状态是否正常,0是新结点入队时的默认状态。

注意waitStatus和state区别分开。

4.总结★

死循环
    尝试获取锁
    判断前置节点waitstatus是否是SIGNAL即-1阻塞等待唤醒,如果前置节点是-1那么自己也进入阻塞
    如果前置节点的waitstatus大于0,说明节点已经被取消,递归断开这些节点返回false。
    继续进入死循环判断前置节点waitstatus
    如果此时前置节点的waitstatus是0
    将当前节点的前置节点即头结点改为-1,返回false。
    
    继续进入死循环判断前置节点状态,此时前置节点的waitstatus是-1,那么自己也进入阻塞返回true。
    进入parkAndCheckInterrupt,被park。
    就算当前线程被打断,也会重新进入死循环,重新进入park。
    这也是不可打断模式的来历。

非公平锁解锁流程

1.释放锁

state--,判断state==0说明释放锁成功

2.获取下一个需要唤醒的节点

获取头结点,头结点是哑结点,注意Thread-1并没有入队。头结点的waitstatus是-1,设置它的waitstatus为0

获取头结点的下一个节点,判断头结点的下一个节点是否大于0。

如果小于等于0,那么就要唤醒头结点的下一个节点。

如果大于0,那么需要从尾部往前找,注意是需要从尾部往前找,注意是需要从尾部往前找

直到找到1个小于等于0的节点,这就是需要唤醒的节点K,这里也是非公平的体现。

注意:在获取锁的时候断开取消的节点的时候也是从后往前断开的

3.unpark需要唤醒节点的线程

unpark节点K所在的线程。

4.思考

在解锁的时候,thread-1所在的node节点并没有被释放,那它就一直作为头结点吗?

不是的,在thread-2加锁成功的时候,会将Thread-2所在的节点作为头结点,并断开thread-1所在的node节点。

具体代码在setHead(node);前后。

为什么unparkSuccessor方法中头结点的下一个节点是无效节点时,从尾节点开始从后往前找?

参考:blog.csdn.net/lsgqjh/arti…

先看下入队的代码enq方法和addWaiter方法:入队的时候都是先设置prev再设置next。

如果从前往后找:那么可能会出现一种情况:

1.当前节点的prev指针指向了原来的尾结点

2.设置当前节点为尾结点 image.png

3.但是原来的尾结点的next还没有指向当前节点,这个时候找到的节点就是null!!!

private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                // 第一次进入 head = new Node()
                // head节点的属性:thread=null
                if (compareAndSetHead(new Node()))
                    //第一次进入,将head赋值给tail,很重要
                    tail = head;
            } else {
                //第二次进入,此处的t虽然是tail ,但是第一次进入的时候将head赋值给tail,这里就是head
                //此处就形成了双向链表 node.prev = head
                
                //注意这里 node.prev = t; 
                //是将原来的尾节点赋值给当前新创建节点的前置节点
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    //此处就形成了双向链表  head.next = node;
                    t.next = node;
                    return t;
                }
            }
        }
    }
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

unparkSuccessor()在查找head的下一个有效节点的时候,没有从head到tail方向查找,而是反方向从tail向head查找,如果你对我刚才分析得到逻辑理解透彻的话,就比较好解释了。

比如:t1设置prev指向Node1,然后cas操作将tail指向了t1,这时Queue的结构如下:

image.png

假如这时候执行unparkSuccessor(),Node0查找它的后驱节点为Node1,假如Node1是无效节点,Node1需要继续查找它的后驱节点,但是这时Node1的next并没有设置,是无法查找到的,所以必须从tail向head方向查找才行。

还有1个地方:在unparkSuccessor方法中

 private void unparkSuccessor(Node node) {
        //node是头结点
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        //s是头结点的后继节点
        Node s = node.next;
        //如果s是空 其实这里也说明了为什么要从后往前找 s=null 没法从前往后遍历
        //那s==null怎么解决呢?
        //答案在入队的时候enq方法
        if (s == null || s.waitStatus > 0) {
            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);
    }
private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            //第一轮:如果队列中还没有元素 tail 为 null
            if (t == null) {
                //第一轮:使用cas 将 head 从 null 变为 新创建的这个空节点 即new Node()
                // head节点的属性:thread=null
                if (compareAndSetHead(new Node()))
                    //第一轮:将head赋值为tail
                    //第一轮:此时head和tail都是新创建的节点
                    //第一轮:进入第二轮循环
                    tail = head;
            } else {
                // 第二轮:插入新节点
                // 第二轮:将原来的tail赋值给新的node的prev 
                // 第二轮:也就是把原来的尾节点放到倒数第二的位置 新节点作为尾节点
                // 第二轮:即新节点的前置节点是 原来的尾节点
                node.prev = t;
                // 也就是将node作为新的tail 即尾节点
                if (compareAndSetTail(t, node)) {
                    // 原来tail的next 设置为 node
                    t.next = node;
                    return t;
                }
            }
        }
    }
//第一次循环
//head = yummy节点
//tail = yummy节点//第二次循环
//新节点的前置节点指向head
//尾节点被设置为新节点
//头结点的后置节点指向新节点
//此时
//head ← 新节点
//tail = 新节点
//head → 新节点
//因为尾节点就是新节点
//所以就是
//head→tail(新节点)
//head←tail(新节点)

非公平锁实现原理

加锁:不去检查同步队列直接去获取锁

final boolean nonfairTryAcquire(int acquires) {
    //acquires=1
    final Thread current = Thread.currentThread();
    int c = getState();
    //如果还没有线程获得锁 
    if (c == 0) {
        // 尝试用 cas 获得, 这里体现了非公平性: 不去检查 AQS 队列
        // 非公平锁可以提供并发度 但是会饥饿
        // 线程切换的开销,其实就是非公平锁效率高于公平锁的原因
        // 因为非公平锁减少了线程挂起的几率,后来的线程有一定几率逃离被挂起的开销
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            //代表加锁成功
            return true;
        }
    }
    // 如果已经获得了锁, 即持有锁的线程是当前线程, 表示发生了锁重入
    else if (current == getExclusiveOwnerThread()) {
        //使用原来的state + acquires 即acquires = 1
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    // 获取失败, 回到代码块3 acquireQueued(addWaiter(Node.EXCLUSIVE), arg) 方法
    return false;
}

解锁:从前往后找待唤醒的节点,如果是取消的节点,那么从后往前找

1.释放锁

state--,判断state==0说明释放锁成功

2.获取下一个需要唤醒的节点

获取头结点,头结点是哑结点,注意Thread-1并没有入队。头结点的waitstatus是-1,设置它的waitstatus为0

获取头结点的下一个节点,判断头结点的下一个节点是否大于0。

如果小于等于0,那么就要唤醒头结点的下一个节点。

如果大于0,那么需要从尾部往前找,注意是需要从尾部往前找,注意是需要从尾部往前找

直到找到1个小于等于0的节点,这就是需要唤醒的节点K,这里也是非公平的体现。

注意:在获取锁的时候断开取消的节点的时候也是从后往前断开的哦

3.unpark需要唤醒节点的线程

unpark节点K所在的线程。

公平锁实现原理

同步队列没有前驱节点, 才去竞争加锁。

否则直接返回加锁失败

    private static final long serialVersionUID = -3000897897090466540L;
​
    final void lock() {
        acquire(1);
    }
​
    // AQS 继承过来的方法, 方便阅读, 放在此处
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
                  acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
            selfInterrupt();
        }
    }
​
    // 与非公平锁主要区别在于 tryAcquire 方法的实现
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            // 先检查 AQS 队列中是否有前驱节点, 没有才去竞争
            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;
    }
​
​
    // ㈠ AQS 继承过来的方法, 方便阅读, 放在此处
    public final boolean hasQueuedPredecessors() {
        Node t = tail;
        Node h = head;
        Node s;
// h != t 时表示队列中有 Node
        return h != t &&
                (
// (s = h.next) == null 表示队列中还有没有老二
                        (s = h.next) == null ||
                                // 或者队列中老二线程不是此线程
                                s.thread != Thread.currentThread()
                );
    }

非公平锁可打断原理

一看到线程的interrupt()方法,根据字面意思,很容易将该方法理解为中断线程。

其实Thread.interrupt()并不会中断线程的运行,它的作用仅仅是为线程设定一个状态而已,即标明线程是中断状态,这样线程的调度机制或我们的代码逻辑就可以通过判断这个状态做一些处理,比如sleep()方法会抛出异常,或是我们根据isInterrupted()方法判断线程是否处于中断状态,然后做相关的逻辑处理。

如果一个线程处于了阻塞状态(如线程调用了thread.sleep、thread.join、thread.wait、condition.await、以及可中断的通道上的 I/O 操作方法后可进入阻塞状态),则线程会检查中断状态标示,如果发现中断状态标示为true,则会在这些阻塞方法(sleep、join、wait、1.5中的condition.await及可中断的通道上的 I/O 操作方法)调用处抛出InterruptedException异常,并且在抛出异常后立即将线程的中断标示位清除,即重新设置为false

抛出异常是为了线程从阻塞状态醒过来,并在结束线程前让程序员有足够的时间来处理中断请求。

注,synchronized在获锁的过程中是不能被中断的,意思是说如果产生了死锁,则不可能被中断(请参考后面的测试例子)。与synchronized功能相似的reentrantLock.lock()方法也是一样,它也不可中断的,即如果发生死锁,那么reentrantLock.lock()方法无法终止,如果调用时被阻塞,则它一直阻塞到它获取到锁为止。

但是如果调用带超时的tryLock方法reentrantLock.tryLock(long timeout, TimeUnit unit),那么如果线程在等待时被中断,将抛出一个InterruptedException异常,这是一个非常有用的特性,因为它允许程序打破死锁。

你也可以调用reentrantLock.lockInterruptibly()方法,它就相当于一个超时设为无限的tryLock方法。

不可打断模式:死循环获取锁才能返回

在此模式下,即使它被打断,仍会驻留在 AQS 队列中,一直要等到获得锁后方能得知自己被打断了

    public final void acquire(int arg) {
        //acquireQueued返回的是打断状态
        //如果打断状态为true 说明 在等待的时候已经被打断
        //在获取到锁以后执行 Thread.currentThread().interrupt();
        if (!tryAcquire(arg) &&
                        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
            // 如果打断状态为 true
            selfInterrupt();
        }
    }
​
    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;
                    failed = false;
                    // 还是需要获得锁后, 才能返回打断状态
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node)
                    && parkAndCheckInterrupt() ) {
                    // 如果是因为 interrupt 被唤醒, 打断状态会被设置为 true
                    // 继续进入循环 又被park
                    interrupted = true;
                }
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
​
  
     private final boolean parkAndCheckInterrupt() {
    // 如果打断的正在运行的线程,则会设置打断标记为true;
    // park 的线程被打断,也会重新设置打断标记为true
    // 如果打断标记已经是 true, 则 park 会失效
        LockSupport.park(this);
    // interrupted方法会返回打断标记 并重置当前线程的打断标记为false
    // 即返回true 并重置为 false
        return Thread.interrupted();
    }
​
    static void selfInterrupt() {
    // 重新产生一次中断 重置打断状态为false 保证下次进入park仍然能被阻塞!
    //这里为什么要调用interrupted() 而不是isInterrupted() ?
    //interrupted会重置打断标记为false 而isInterrupted只是返回打断标记
    //当park的线程在被调用interrupt方法时,会把中断状态设置为true。
    //然后park方法会去判断中断状态,如果为true,就直接返回,然后往下继续执行,如果为false继续阻塞
        Thread.currentThread().interrupt();
    }
​

可打断模式:打断唤醒后抛出异常

 public final void acquireInterruptibly(int arg) throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
// 如果没有获得到锁, 进入 ㈠
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }
​
    // ㈠ 可打断的获取锁流程
    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;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt()) {
                // 在 park 过程中如果被 interrupt 会进入此
                // 这时候抛出异常, 而不会再次进入 for (;;)
                    throw new InterruptedException();
                }
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
}
​
 private final boolean parkAndCheckInterrupt() {
    // 如果打断的正在运行的线程,则会设置打断标记为true;
    // park 的线程被打断,也会重新设置打断标记为true
    // 如果打断标记已经是 true, 则 park 会失效
        LockSupport.park(this);
    // interrupted方法会返回打断标记true 并重置当前线程的打断标记为false
    // 即返回true 并重置为 false
        return Thread.interrupted();
    }