多线程进阶学习10------AQS详解

2023-10-26

AbstractQueuedSynchronizer,来自于JDK1.5,位于JUC包,由并发编程大师Doug Lea编写,字面翻译就是“抽象队列同步器”,简称为AQS。AQS作为一个抽象类,是构建JUC包中的锁(比如ReentrantLock)或者其他同步组件(比如CountDownLatch)的底层基础框架。

这个框架有几个重要的模块:

state:同步量,所有线程是通过竞争他,来判断是否加锁成功。既然是以它为标准,那当然需要保证可见性。所以state用volatie修饰。

同步队列:竞争同步量调用acquire失败后进入的一个等待队列,由双向链表组成。

condition:光只有锁还不够,我们在多线程间做线程同步,有更多复杂的需求,哪怕我获取锁了,但是我还需要等待另一个线程给我加工些东西,那么我还需要进入等待。我们可以通过condition.await来进入条件队列进行等待并且释放锁,等其他线程signal该线程。

条件队列:等待条件进入的就是条件队列,一个锁可以生成多个condition对象,也就是可以等待多种条件来进行线程同步。条件可以有很多个,条件队列也可以有很多个。这样线程间能根据不同条件进行更复杂的并发控制。

LockSuport:获取锁失败,或者获取条件,我们线程都会进入一个队列进行阻塞等待。那么阻塞这个操作,就依赖我们的基础工具LockSuport.park来支持。
那么接下来,我们就深入了解这些模块

AbstractQueuedSynchronizer抽象类

属性

//指向同步队列的哨兵头结点
private transient volatile Node head;
//指向同步队列的尾节点
private transient volatile Node tail;
//最重要的同步量
private volatile int state;
//用来保存当前获取锁的线程,以便后期判断。需要实现者去设置值
private transient Thread exclusiveOwnerThread;

需重写方法

/**
 * 独占式获取锁,该方法需要查询当前状态并判断锁是否符合预期,然后再进行CAS设置锁。返回true则成功,否则失败。
 *
 * @param arg 参数,在实现的时候可以传递自己想要的值
 * @return 返回true则成功,否则失败。
 */
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

/**
 * 独占式释放锁,等待获取锁的线程将有机会获取锁。返回true则成功,否则失败。
 *
 * @param arg 参数,在实现的时候可以传递自己想要的数据
 * @return 返回true则成功,否则失败。
 */
protected boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();
}

/**
 * 共享式获取锁,返回大于等于0的值表示获取成功,否则失败。
 *
 * @param arg 参数,在实现的时候可以传递自己想要的数据
 * @return 返回大于等于0的值表示获取成功,否则失败。
 * 如果返回值小于0,表示当前线程共享锁失败
 * 如果返回值大于0,表示当前线程共享锁成功,并且接下来其他线程尝试获取共享锁的行为很可能成功
 * 如果返回值等于0,表示当前线程共享锁成功,但是接下来其他线程尝试获取共享锁的行为会失败(实际上也有可能成功,在后面的源码部分会将)
 */
protected int tryAcquireShared(int arg) {
    throw new UnsupportedOperationException();
}

/**
 * 共享式释放锁。返回true成功,否则失败。
 *
 * @param arg 参数,在实现的时候可以传递自己想要的数据
 * @return 返回true成功,否则失败。
 */
protected boolean tryReleaseShared(int arg) {
    throw new UnsupportedOperationException();
}

/**
 * 判断当前线程是否独占锁。比如我们调用signal方法,就需要线程独占锁才能触发条件通知
 *
 * @return 如果同步是以独占方式进行的,则返回true;其他情况则返回 false
 */
protected boolean isHeldExclusively() {
    throw new UnsupportedOperationException();
}

供调用方法

这个框架提供给子类直接调用的模板方法都是final修饰的,不让子类重写。
它们又分为三类:

独占方式

/**
* 独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,
* 否则,将会进入同步队列等待。
* 该方法不会响应中断。该方法内部调用了可重写的tryAcquire方法。
*/
public final void acquire(int arg)
/**
*与acquire方法相同,但是该方法响应中断,当前线程未获取到同步状态而进入同步队列中,
*如果当前被中断,则该方法会抛出InterruptedException并返回。
*/
public final void acquireInterruptibly(int arg)(int arg)
/**
*在acquireInterruptibly方法基础上增加了超时限制,
* 如果当前线程在超时时间内没有获取到同步状态,
* 那么将会返回false,获取到了返回true。
*/
public final boolean tryAcquireNanos(int arg,long nanos)
/**
*独占式的释放同步状态,该方法会在释放同步状态之后
* 将同步队列中第一个结点包含的线程唤醒。
* 该方法内部调用了可重写的tryRelease方法。
*/
public final boolean release(int arg)

共享方式

/**
 * 共享式获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待。
 * 与独占式的不同是同一时刻可以有多个线程获取到同步状态。该方法不会响应中断。
 * 该方法内部调用了可重写的tryAcquireShared方法。
 */
public final void acquireShared(int arg)

/**
 * 与acquireShared (int arg) 相同,但是该方法响应中断,
 * 当前线程未获取到同步状态而进入同步队列中,
 * 如果当前被中断,则该方法会抛出InterruptedException并返回。
 */
public final void acquireSharedInterruptibly(int arg)
/**
 *在acquireSharedInterruptibly方法基础上增加了超时限制,
 * 如果当前线程在超时时间内没有获取到同步状态,
 * 那么将会返回false,获取到了返回true。
 */
public final boolean tryAcquireSharedNanos(int arg,long nanos)
/**
 *共享式释放同步状态,该方法会在释放同步状态之后,尝试唤醒同步队列中的后继节点中的线程。
 * 该方法内部调用了可重写的tryReleaseShared方法。
 */
public final boolean releaseShared(int arg)

获取线程等待情况

//有些子类有需求,想要获取同步队列上的所有线程,让用户好操作
public final Collection<Thread> getQueuedThreads()

加锁解锁流程

加锁失败会进入一个同步队列,我们先看看这个队列的结构


public abstract class AbstractQueuedSynchronizer extends
            AbstractOwnableSynchronizer implements java.io.Serializable {
        /**
         * 当前获取锁的线程,该变量定义在父类中,AQS直接继承。在独占锁的获取时,如果是重入锁,那么需要知道到底是哪个线程获得了锁。没有就是null
         */
        private transient Thread exclusiveOwnerThread;
        /**
         * AQS中保持的对同步队列的引用
         * 队列头结点,实际上是一个哨兵结点,不代表任何线程,head所指向的Node的thread属性永远是null。
         */
        private transient volatile Node head;
        /**
         * 队列尾结点,后续的结点都加入到队列尾部
         */
        private transient volatile Node tail;
        /**
         * 同步状态
         */
        private volatile int state;

        /**
         * Node内部类,同步队列的结点类型
         */
        static final class Node {

            /*AQS支持共享模式和独占模式两种类型,下面表示构造的结点类型标记*/
            /**
             * 共享模式下构造的结点,用来标记该线程是获取共享资源时被阻塞挂起后放入AQS 队列的
             */
            static final Node SHARED = new Node();
            /**
             * 独占模式下构造的结点,用来标记该线程是获取独占资源时被阻塞挂起后放入AQS 队列的
             */
            static final Node EXCLUSIVE = null;


            /*线程结点的等待状态,用来表示该线程所处的等待锁的状态*/

            /**
             * 指示当前结点(线程)需要取消等待
             * 由于在同步队列中等待的线程发生等待超时、中断、异常,即放弃获取锁,需要从同步队列中取消等待,就会变成这个状态
             * 如果结点进入该状态,那么不会再变成其他状态
             */
            static final int CANCELLED = 1;
            /**
             * 指示当前结点(线程)的后续结点(线程)需要取消等待(被唤醒)
             * 如果一个结点状态被设置为SIGNAL,那么后继结点的线程处于挂起或者即将挂起的状态
             * 当前结点的线程如果释放了锁或者放弃获取锁并且结点状态为SIGNAL,那么将会尝试唤醒后继结点的线程以运行
             * 这个状态通常是由后继结点给前驱结点设置的。一个结点的线程将被挂起时,会尝试设置前驱结点的状态为SIGNAL
             */
            static final int SIGNAL = -1;
            /**
             * 线程在等待队列里面等待,waitStatus值表示线程正在等待条件
             * 原本结点在等待队列中,结点线程等待在Condition上,当其他线程对Condition调用了signal()方法之后
             * 该结点会从从等待队列中转移到同步队列中,进行同步状态的获取
             */
            static final int CONDITION = -2;
            /**
             * 释放共享资源时需要通知其他结点,waitStatus值表示下一个共享式同步状态的获取应该无条件传播下去
             */
            static final int PROPAGATE = -3;
            /**
             * 记录当前线程等待状态值,包括以上4中的状态,还有0,表示初始化状态
             */
            volatile int waitStatus;

            /**
             * 前驱结点,当结点加入同步队列将会被设置前驱结点信息
             */
            volatile Node prev;

            /**
             * 后继结点
             */
            volatile Node next;

            /**
             * 当前获取到同步状态的线程
             */
            volatile Thread thread;

            /**
             * 等待队列中的后继结点,如果当前结点是共享模式的,那么这个字段是一个SHARED常量
             * 在独占锁模式下永远为null,仅仅起到一个标记作用,没有实际意义。
             */
            Node nextWaiter;

            /**
             * 如果是共享模式下等待,那么返回true(因为上面的Node nextWaiter字段在共享模式下是一个SHARED常量)
             */
            final boolean isShared() {
                return nextWaiter == SHARED;
            }

            /**
             * 用于建立初始头结点或SHARED标记
             */
            Node() {
            }

            /**
             * 用于添加到等待队列
             *
             * @param thread
             * @param mode
             */
            Node(Thread thread, Node mode) {
                this.nextWaiter = mode;
                this.thread = thread;
            }
            //......
        }
    }
}

在这里插入图片描述
它的head引用指向的头结点作为哨兵结点,不存储任何与等待线程相关的信息,或者可以当作是已经获得锁的结点。第二个结点开始才是真正的等待线程构建的结点,后续的结点会加入到链表尾部。

入尾部的操作是采用cas的方式,能保证线程安全。

线程在释放锁的时候,会调用release,子类去管理state值的重置,模板骨架会从同步队列找出最早的一个来唤醒。

独占锁加锁

通过调用AQS的acquire模版方法可以独占式的获取锁,该方法不会响应中断,也就是由于线程获取同步状态失败后进入同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移出。基于独占式实现的组件有ReentrantLock等。

该方法大概步骤如下:

  1. 首先调用tryAcquire方法尝试获取锁,如果获取锁成功会返回true,方法结束;否则获取锁失败返回false,然后进行下一步的操作。
  2. 通过addWaiter方法将线程按照独占模式Node.EXCLUSIVE构造同步结点,并添加到同步队列的尾部。
  3. 然后通过acquireQueued(Node node,int arg)方法继续自旋获取锁。
  4. 一次自旋中如果获取不到锁,那么判断是否可以挂起并尝试挂起结点中的线程(调用LockSupport.park(this)方法挂起自己,注意这里的线程状态是WAITING)。而挂起线程的唤醒主要依靠前驱结点或线程被中断来实现,注意唤醒之后会继续自旋尝试获得锁。
  5. 终只有获得锁的线程才能从acquireQueued方法返回,然后根据返回值判断是否调用selfInterrupt设置中断标志位,但此时线程处于运行态,即使设置中断标志位也不会抛出异常(即acquire(lock)方法不会响应中断)。
  6. 线程获得锁,acquire方法结束,从lock方法中返回,继续后续执行同步代码!
/**
 * 独占式的尝试获取锁,一直获取不成功就进入同步队列等待
 */
public final void acquire(int arg) {
    //内部是由4个方法的调用组成的
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

tryAcquire尝试获取独占锁

这就是子类去重写的尝试获取独占锁的方法,在很多场景会复用到该方法

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

addWaiter加入到同步队列

addWaiter方法是AQS提供的,也不需要我们重写,或者说是锁的通用方法!

addWaiter方法用于将按照独占模式构造的同步结点Node.EXCLUSIVE添加到同步队列的尾部。大概步骤为:

按照给定模式,构建新结点。

如果同步队列不为null,则尝试将新结点添加到队列尾部(只尝试一次),如果添加成功则返回新结点,方法结束。

如果队列为null或者添加失败,则调用enq方法循环尝试添加,直到成功,返回新结点,方法结束。

/**
 * addWaiter(Node node)方法将获取锁失败的线程构造成结点加入到同步队列的尾部
 *
 * @param mode 模式。独占模式传入的是一个Node.EXCLUSIVE,即null;共享模式传入的是一个Node.SHARED,即一个静态结点对象(共享的、同一个)
 * @return 返回构造的结点
 */
private Node addWaiter(Node mode) {
    /*1 首先构造结点*/
    Node node = new Node(Thread.currentThread(), mode);
    /*2 尝试将结点直接放在队尾*/
    //直接获取同步器的tail结点,使用pred来保存
    Node pred = tail;
    /*如果pred不为null,实际上就是队列不为null
     * 那么使用CAS方式将当前结点设为尾结点
     * */
    if (pred != null) {
        node.prev = pred;
        //通过使用compareAndSetTail的CAS方法来确保结点能够被线程安全的添加,虽然不一定能成功。
        if (compareAndSetTail(pred, node)) {
            //将新构造的结点置为原队尾结点的后继
            pred.next = node;
            //返回新结点
            return node;
        }
    }
    /*
     * 3 走到这里,可能是:
     * (1) 由于可能是并发条件,并且上面的CAS操作并没有循环尝试,因此可能添加失败
     * (2) 队列可能为null
     * 调用enq方法,采用自旋方式保证构造的新结点成功添加到同步队列中
     * */
    enq(node);
    return node;
}

/**
 * addWaiter方法中使用到的Node构造器
 *
 * @param thread 当前线程
 * @param mode   模式
 */
Node(Thread thread, AbstractQueuedSynchronizer.Node mode) {
    //等待队列中的后继结点 就等于该结点的模式
    //由此可知,共享模式该值为Node.SHARED结点常量,独占模式该值为null
    this.nextWaiter = mode;
    //当前线程
    this.thread = thread;
}

enq保证结点入队

enq做两件事
1.如果没初始化,可以初始化队列,生成哨兵节点
2.循环保证一定插入成功

/**
 * 循环,直到尾结点添加成功
 */
private Node enq(final Node node) {
    /*死循环操作,直到添加成功*/
    for (; ; ) {
        //获取尾结点t
        Node t = tail;
        /*如果队列为null,则初始化同步队列*/
        if (t == null) {
            /*调用compareAndSetHead方法,初始化同步队列
             * 注意:这里是新建了一个空白结点,这就是传说中的哨兵结点
             * CAS成功之后,head将指向该哨兵结点,返回true
             * */
            if (compareAndSetHead(new Node()))
                //尾结点指向头结点(哨兵结点)
                tail = head;
            /*之后并没有结束,而是继续循环,此时队列已经不为空了,因此会进行下面的逻辑*/
        }
        /*如果队列不为null,则和外面的的方法类似,调用compareAndSetTail方法,新建新结点到同步队列尾部*/
        else {
            /*1 首先修改新结点前驱的指向,这一步不是安全的
            但是没关系,因为这一步如果发生了冲突,那么下面的CAS操作必然之后有一条线程会成功
            其他线程将会重新循环尝试*/
            node.prev = t;
            /*
             * 2 调用compareAndSetTail方法通过CAS方式尝试将结点添加到同步队列尾部
             * 如果添加成功,那么才能继续下一步,结束这个死循环,否则就会不断循环尝试添加
             * */
            if (compareAndSetTail(t, node)) {
                //3 修改原尾结点后继结点的指向
                t.next = node;
                //返回新结点,结束死循环
                return t;
            }
        }
    }
}

/**
 * CAS添加头结点. 仅仅在enq方法中用到
 *
 * @param update 头结点
 * @return true 成功;false 失败
 */
private final boolean compareAndSetHead(Node update) {
    return unsafe.compareAndSwapObject(this, headOffset, null, update);
}


/**
 * CAS添加尾结点. 仅仅在enq方法中用到
 *
 * @param expect 预期原尾结点
 * @param update 新尾结点
 * @return true 成功;false 失败
 */
private final boolean compareAndSetTail(Node expect, Node update) {
    return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}

acquireQueued结点自旋获取锁

能够走到该方法,那么说明通过了tryAcquire()和addWaiter()方法,表示该线程获取锁已经失败并且被放入同步队列尾部了。

acquireQueued方法表示结点进入同步队列之后的动作,实际上就进入了一个自旋的过程,自旋过程中,当条件满足,获取到了锁,就可以从这个自旋中退出并返回,否则可能会阻塞该结点的线程,后续即使阻塞被唤醒,还是会自旋尝试获取锁,直到成功或者而抛出异常。

最终如果该方法会因为获取到锁而退出,则会返回否被中断标志的标志位 或者 因为异常而退出,则会抛出异常!大概步骤为:

  1. 同样开启一个死循环,在死循环中进行下面的操作;
  2. 如果当前结点的前驱是head结点,那么尝试获取锁,如果获取锁成功,那么当前结点设置为头结点head,当前结点线程出队,表示当前线程已经获取到了锁,然后返回是否被中断标志,结束循环,进入finally;
  3. 如果当前结点的前驱不是head结点或者尝试获取锁失败,那么判断当前线程是否应该被挂起,如果返回true,那么调用parkAndCheckInterrupt挂起当前结点的线程(LockSupport.park 方法挂起线程,线程出于WAITING),此时不再执行后续的步骤、代码。
  4. 如果当前线程不应该被挂起,即返回false,那本次循环结束,继续下一次循环。
  5. 如果线程被其他线程唤醒,那么判断是否是因为中断而被唤醒并修改标志位,同时继续循环,直到在步骤2获得锁,才能跳出循环!(这也是acquire方法不会响应中断的原理—park方法被中断时不会抛出异常,仅仅是从挂起状态返回,然后需要继续尝试获取锁)
  6. 最终,线程获得了锁跳出循环,或者发生异常跳出循环,那么会执行finally语句块,finally中判断线程是否是因为发生异常而跳出循环,如果是,那么执行cancelAcquire方法取消该结点获取锁的请求;如果不是,即因为获得锁跳出循环,则finally中什么也不干!
/**
 * @param node 新结点
 * @param arg  参数
 * @return 如果在等待时中断,则返回true
 */
final boolean acquireQueued(final Node node, int arg) {
    //failed表示获取锁是否失败标志
    boolean failed = true;
    try {
        //interrupted表示是否被中断标志
        boolean interrupted = false;
        /*死循环*/
        for (; ; ) {
            //获取新结点的前驱结点
            final Node p = node.predecessor();
            /*只有前驱结点是头结点的时候才能尝试获取锁
             * 同样调用tryAcquire方法获取锁
             * */
            if (p == head && tryAcquire(arg)) {
                //获取到锁之后,就将自己设置为头结点(哨兵结点),线程出队列
                setHead(node);
                //前驱结点(原哨兵结点)的链接置空,由JVM回收
                p.next = null;
                //获取锁是否失败改成false,表示成功获取到了锁
                failed = false;
                //返回interrupted,即返回线程是否被中断
                return interrupted;
            }
            /*前驱结点不是头结点或者获取同步状态失败*/
            /*shouldParkAfterFailedAcquire检测线程是否应该被挂起,如果返回true
             * 则调用parkAndCheckInterrupt用于将线程挂起
             * 否则重新开始循环
             * */
            if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                /*到这一步,说明是当前结点(线程)因为被中断而唤醒,那就改变自己的中断标志位状态信息为true
                 * 然后又从新开始循环,直到获取到锁,才能返回
                 * */
                interrupted = true;
        }
    }
    /*线程获取到锁或者发生异常之后都会执行的finally语句块*/ finally {
        /*如果failed为true,表示获取锁失败,即对应发生异常的情况,
        这里发生异常的情况只有在tryAcquire方法和predecessor方法中可能会抛出异常,此时还没有获得锁,failed=true
        那么执行cancelAcquire方法,该方法用于取消该线程获取锁的请求,将该结点的线程状态改为CANCELLED,并尝试移除结点(如果是尾结点)
        另外,在超时等待获取锁的的方法中,如果超过时间没有获取到锁,也会调用该方法

        如果failed为false,表示获取到了锁,那么该方法直接结束,继续往下执行;*/
        if (failed)
            //取消获取锁请求,将当前结点从队列中移除,
            cancelAcquire(node);
    }
}


/**
 * 位于Node结点类中的方法
 * 返回上一个结点,或在 null 时引发 NullPointerException。 当前置不能为空时使用。 空检查可以取消,表示此异常无代码层面的意义,但可以帮助 VM?所以这个异常到底有啥用?
 *
 * @return 此结点的前驱
 */
final Node predecessor() throws NullPointerException {
    //获取前驱
    Node p = prev;
    //如果为null,则抛出异常
    if (p == null)
        throw new NullPointerException();
    else
        //返回前驱
        return p;
}


/**
 * head指向node新结点,该方法是在tryAcquire获取锁之后调用,不会产生线程安全问题
 *
 * @param node 新结点
 */
private void setHead(Node node) {
    head = node;
    //新结点的thread和prev属性置空
    //即丢弃原来的头结点,新结点成为哨兵结点,内部线程出队
    //设置里虽然线程引用置空了,但是一般在tryAcquire方法中轨记录获取到锁的线程,因此不担心找不到是哪个线程获取到了锁
    //这里也能看出,哨兵结点或许也可以叫做"获取到锁的结点"
    node.thread = null;
    node.prev = null;
}

shouldParkAfterFailedAcquire结点是否应该挂起

shouldParkAfterFailedAcquire方法在没有获取到锁之后调用,用于判断当前结点是否需要被挂起。大概步骤如下:

  1. 如果前驱结点已经是SIGNAL(-1)状态,即表示当前结点可以挂起,返回true,方法结束;
  2. 否则,如果前驱结点状态大于0,即 Node.CANCELLED,表示前驱结点放弃了锁的等待,那么由该前驱向前查找,直到找到一个状态小于等于0的结点,当前结点排在该结点后面,返回false,方法结束;
  3. 否则,前驱结点的状态既不是SIGNAL(-1),也不是CANCELLED(1),尝试CAS设置前驱结点的状态为SIGNAL(-1),返回false,方法结束!

只有前驱结点状态为SIGNAL时,当前结点才能安心挂起,否则一直自旋!

从这里能看出来,一个结点的SIGNAL状态一般都是由它的后继结点设置的,但是这个状态却是表示后继结点的状态,表示的意思就是前驱结点如果释放了锁,那么就有义务唤醒后继结点!


/**
 * 检测当前结点(线程)是否应该被挂起
 *
 * @param pred 该结点的前驱
 * @param node 该结点
 * @return 如果前驱结点已经是SIGNAL状态,当前结点才能挂起,返回true;否则,可能会查找新的前驱结点或者尝试将前驱结点设置为SIGNAL状态,返回false
 */
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    //获取 前取的waitStatus_等待状态
    //回顾创建结点时候,并没有给waitStatus赋值,因此每一个结点最开始的时候waitStatus的值都为0
    int ws = pred.waitStatus;
    /*如果前驱结点已经是SIGNAL状态,即表示当前结点可以挂起*/
    if (ws == Node.SIGNAL)
        return true;
    /*如果前驱结点状态大于0,即 Node.CANCELLED 表示前驱结点放弃了锁的等待*/
    if (ws > 0) {
        /*由该前驱向前查找,直到找到一个状态小于等于0的结点(即没有被取消的结点),当前结点成为该结点的后驱,这一步很重要,可能会清理一段被取消了的结点,并且如果该前驱释放了锁,还会唤醒它的后继,保持队列活性*/
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    }
    /*否则,前驱结点的状态既不是SIGNAL(-1),也不是CANCELLED(1)*/
    else {
        /*前驱结点的状态CAS设置为SIGNAL(-1),可能失败,但没关系,因为失败之后会一直循环*/
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    //返回false,表示当前结点不能挂起
    return false;
}

parkAndCheckInterrupt挂起线程&判断中断状态

shouldParkAfterFailedAcquire方法返回true之后,将会调用parkAndCheckInterrupt方法挂起线程并且后续判断中断状态,分两步:

  1. 使用LockSupport.park(this)挂起该线程,不再执行后续的步骤、代码。直到该线程被中断或者被唤醒(unpark)!
  2. 如果该线程被中断或者唤醒,那么返回Thread.interrupted()方法的返回值,该方法用于判断前线程的中断状态,并且清除该中断状态,即,如果该线程因为被中断而唤醒,则中断状态为true,将中断状态重置为false,并返回true,如果该线程不是因为中断被唤醒,则中断状态为false,并返回false。
/**
 * 挂起线程,在线程返回后返回中断状态
 *
 * @return 如果因为线程中断而返回,而返回true,否则返回false
 */
private final boolean parkAndCheckInterrupt() {
    /*1)使用LockSupport.park(this)挂起该线程,不再执行后续的步骤、代码。直到该线程被中断或者被唤醒(unpark)*/
    LockSupport.park(this);
    /*2)如果该线程被中断或者唤醒,那么返回Thread.interrupted()方法的返回值,
    该方法用于判断前线程的中断状态,并且清除该中断状态,即,如果该线程因为被中断而唤醒,则中断状态为true,将中断状态重置为false,并返回true,注意park方法被中断时不会抛出异常!
    如果该线程不是因为中断被唤醒,则中断状态为false,并返回false*/
    return Thread.interrupted();
}

finally代码块

在acquireQueued方法中,具有一个finally代码块,那么无论try中发生了什么,finally代码块都会执行的。在acquire独占式不可中断获取锁的方法中,执行finally的只有两种情况:

  1. 当前结点(线程)最终获取到了锁,此时会进入finally,而在获取到锁之后会设置failed = false。

  2. 在try中发生了异常,此时直接跳到finally中。这里发生异常的情况只可能在tryAcquire或predecessor方法中发生,然后直接进入finally代码块中,此时还没有获得锁,failed=true!

    a. tryAcquire方法是我们自己实现的,抛出什么异常由我们来定,就算抛出异常一般也不会在acquireQueued中抛出,可能在最开始调用tryAcquire时就抛出了。

    b.predecessor方法中,会检查如果前驱结点为null则抛出NullPointerException。但是注释中又说这个检查无代码层面的意义,或许是这个异常永远不会抛出?

finally代码块中的逻辑为:

  1. 如果failed = true,表示没有获取锁而进行finally,即发生了异常。那么执行cancelAcquire方法取消当前结点线程获取锁的请求,acquireQueued方法结束,然后抛出异常。
  2. 如果failed = false,表示已经获取到了锁,那么实际上finally中什么都不会执行。acquireQueued方法结束,返回interrupted—是否被中断标志。

综上所述,在acquire独占式不可中断获取锁的方法中,大部分情况在finally中都是什么也不干就返回了,或者说抛出异常的情况基本没有,因此cancelAcquire方法基本不考虑。

但是在可中断获取锁或者超时获取锁的方法中,执行到cancelAcquire方法的情况还是比较常见的。因此将cancelAcquire方法的源码分析放到可中断获取锁方法的源码分析部分!

selfInterrupt自我中断

selfInterrupt是acquire中最后可能调用的一个方法,顾名思义,用于自我中断,什么意思呢,就是根据!tryAcquire和acquireQueued返回值判断是否需要设置中断标志位。

只有tryAcquire尝试失败,并且acquireQueued方法true时,才表示该线程是被中断过了的,但是在parkAndCheckInterrupt里面判断中断标志位之后又重置的中断标志位(interrupted方法会重置中断标志位)。

虽然看起来没啥用,但是本着负责的态度,还是将中断标志位记录下来。那么此时重新设置该线程的中断标志位为true。

/**
 * 中断当前线程,由于此时当前线程出于运行态,因此只会设置中断标志位,并不会抛出异常
 */
static void selfInterrupt() {
    Thread.currentThread().interrupt();
}

独占锁释放锁

当前线程获取到锁并执行了相应逻辑之后,就需要释放锁,使得后续结点能够继续获取锁。通过调用AQS的release(int arg)模版方法可以独占式的释放锁,在该方法大概步骤如下:

  1. 尝试使用tryRelease(arg)释放锁,该方法在最开始我们就讲过,是自己实现的方法,通常来说就是将state值为0或者减少、清除当前获得锁的线程等等,如果符合自己的逻辑,锁释放成功则返回true,否则返回false;
  2. 如果tryRelease释放成功返回true,判断如果head不为null且head的状态不为0,那么尝试调用unparkSuccessor方法唤醒头结点之后的一个非取消状态(非CANCELLED状态)的后继结点,让其可以进行锁获取。返回true,方法结束;
  3. 如果tryRelease释放失败,那么返回false,方法结束。
/**
 * 独占式的释放同步状态
 *
 * @param arg 参数
 * @return 释放成功返回true, 否则返回false
 */
public final boolean release(int arg) {
    /*tryRelease释放同步状态,该方法是自己重写实现的方法
    释放成功将返回true,否则返回false或者自己实现的逻辑*/
    if (tryRelease(arg)) {
        //获取头结点
        Node h = head;
        //如果头结点不为null并且状态不等于0
        if (h != null && h.waitStatus != 0)
            /*那么唤醒头结点的一个出于等待锁状态的后继结点
             * 该方法在acquire中已经讲过了
             * */
            unparkSuccessor(h);
        return true;
    }
    return false;
}

unparkSuccessor唤醒后继结点

unparkSuccessor用于唤醒参数结点的某个非取消的后继结点,该方法在很多地方法都被调用,大概步骤:

  1. 如果当前结点的状态小于0,那么CAS设置为0,表示后继结点可以继续尝试获取锁
  2. 如果当前结点的后继s为null或者状态为取消CANCELLED,则将s先指向null;然后从tail开始到node之间倒序向前查找,找到离tail最远的非取消结点赋给s。需要从后向前遍历,因为同步队列只保证结点前驱关系的正确性。
  3. 如果s不为null,那么状态肯定不是取消CANCELLED,则直接唤醒s的线程,调用LockSupport.unpark方法唤醒,被唤醒的结点将从被park的位置继续执行!
/**
 * 唤醒指定结点的后继结点
 *
 * @param node 指定结点
 */
private void unparkSuccessor(Node node) {

    int ws = node.waitStatus;
    /*
     * 1)  如果当前结点的状态小于0,那么CAS设置为0,表示后继结点线程可以先尝试获锁,而不是直接挂起。
     * */
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    //先获取node的直接后继
    Node s = node.next;
    /*
     * 2)  如果s为null或者状态为取消CANCELLED,则从tail开始到node之间倒序向前查找,找到离tail最远的非取消结点赋给s。
     * */
    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;
    }
    /*
     * 3)如果s不为null,那么状态肯定不是取消CANCELLED,则直接唤醒s的线程,调用LockSupport.unpark方法唤醒,被唤醒的结点将从被park的位置向后执行!
     * */
    if (s != null)
        LockSupport.unpark(s.thread);
}

其他方法

至于其他的加锁可中断,和加锁可超时,其实都和我们之前的那些基础api有共通点,就不细说了

public final void acquireInterruptibly(int arg)
        throws InterruptedException {
    //如果当前线程被中断,直接抛出异常
    if (Thread.interrupted())
        throw new InterruptedException();
    //尝试获取锁
    if (!tryAcquire(arg))
        //如果没获取到,那么调用AQS 可被中断的方法
        doAcquireInterruptibly(arg);
}
/**
 * 独占式超时获取锁,支持中断
 *
 * @param arg          参数
 * @param nanosTimeout 超时时间,纳秒
 * @return 是否获取锁成功
 * @throws InterruptedException 如果被中断,则抛出InterruptedException异常
 */
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    //如果当前线程被中断,直接抛出异常
    if (Thread.interrupted())
        throw new InterruptedException();
    //同样调用tryAcquire尝试获取锁,如果获取成功则直接返回true
    //否则调用doAcquireNanos方法挂起指定一段时间,该短时间内获取到了锁则返回true,超时还未获取到锁则返回false
    return tryAcquire(arg) ||
            doAcquireNanos(arg, nanosTimeout);//底层原理就是调用parkNacos()
}

共享锁

读懂了独占锁,共享锁就比较简单了。

1.共享锁的模式是shared的。

2.如果是共享节点被唤醒,会根据传播状态,继续向后唤醒下一个节点。那会不会把独占节点也唤醒了?

答案是会的,但是独占节点被唤醒后依然会去抢锁tryAcquireShared。这时根据子类的实现,独占节点是抢不到锁的,所以又会继续阻塞。

这样虽然看似耗费性能,实则更加通用。

/**
 * 自旋尝试共享式获取锁,一段时间后可能会挂起
 * 和独占式获取的区别:
 * 1 以共享模式Node.SHARED添加结点
 * 2 获取到锁之后,修改当前的头结点,并将信息传播到后续的结点队列中
 *
 * @param arg 参数
 */
private void doAcquireShared(int arg) {
    /*1 addWaiter方法逻辑,和独占式获取的区别1 :以共享模式Node.SHARED添加结点*/
    final Node node = addWaiter(Node.SHARED);
    /*2 下面就是类似于acquireQueued方法的逻辑
     * 区别在于获取到锁之后acquireQueued调用setHead方法,这里调用setHeadAndPropagate方法
     *  */
    //当前线程获取锁失败的标志
    boolean failed = true;
    try {
        //当前线程的中断标志
        boolean interrupted = false;
        for (; ; ) {
            //获取前驱结点
            final Node p = node.predecessor();
            /*当前驱结点是头结点的时候就会以共享的方式去尝试获取锁*/
            if (p == head) {
                int r = tryAcquireShared(arg);
                /*返回值如果大于等于0,则表示获取到了锁*/
                if (r >= 0) {
                    /*和独占式获取的区别2 :修改当前的头结点,根据传播状态判断是否要唤醒后继结点。*/
                    setHeadAndPropagate(node, r);
                    // 释放掉已经获取到锁的前驱结点
                    p.next = null;
                    /*检查设置中断标志*/
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            /*判断是否应该挂起,以及挂起的方法,和acquireQueued方法的逻辑完全一致,不会响应中断*/
            if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

再看看释放锁的逻辑,主要是调用doReleaseShared方法,只会唤醒一个头线程。然后唤醒的线程会在doAcquireShared循环获取锁后继续往后唤醒。

/**
 * 共享式获取锁的核心方法,尝试唤醒一个后继线程,被唤醒的线程会尝试获取共享锁,如果成功之后,则又会有可能调用setHeadAndPropagate,将唤醒传播下去。
 * 独占锁只有在一个线程释放所之后才会唤醒下一个线程,而共享锁在一个线程在获取到锁和释放掉锁锁之后,都可能会调用这个方法唤醒下一个线程
 * 因为在共享锁模式下,锁可以被多个线程所共同持有,既然当前线程已经拿到共享锁了,那么就可以直接通知后继结点来获取锁,而不必等待锁被释放的时候再通知。
 */
private void doReleaseShared() {
    /*一个死循环,跳出循环的条件就是最下面的break*/
    for (; ; ) {
        //获取当前的head,每次循环读取最新的head
        Node h = head;
        //如果h不为null且h不为tail,表示队列至少有两个结点,那么尝试唤醒head后继结点线程
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            //如果头结点的状态为SIGNAL,那么表示后继结点需要被唤醒
            if (ws == Node.SIGNAL) {
                //尝试CAS设置h的状态从Node.SIGNAL变成0
                //可能存在多线程操作,但是只会有一条成功
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    //失败的线程结束本次循环,继续下一次循环
                    continue;            // loop to recheck cases
                //成功的那一条线程会调用unparkSuccessor方法唤醒head的一个没有取消的后继结点
                //对于一个head,只需要一条线程去唤醒该head的后继就行了。上面的CAS就是保证unparkSuccessor方法对于一个head只执行一次
                unparkSuccessor(h);
            }
            /*
             * 如果h状态为0,那说明后继结点线程已经是唤醒状态了或者将会被唤醒,不需要该线程来唤醒
             * 那么尝试设置h状态从0变成PROPAGATE,如果失败则继续下一次循环,此时设置PROPAGATE状态能保证唤醒操作能够传播下去
             * 因为后继结点成为头结点时,在setHeadAndPropagate方法中能够读取到原head结点的PROPAGATE状态<0,从而让它可以尝试唤醒后继结点(如果存在)
             * */
            else if (ws == 0 &&
                    !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                //失败的线程结束本次循环,继续下一次循环
                continue;                // loop on failed CAS
        }
        // 执行到这一步说明在上面的判断中队列可能只有一个结点,或者unparkSuccessor方法调用完毕,或h状态为PROPAGATE(不需要继续唤醒后继)
        // 再次检查h是否仍然是最新的head,如果不是的话需要再进行循环;如果是的话说明head没有变化,退出循环
        if (h == head)                   // loop if head changed
            break;
    }
}

等待条件与通知流程

lock相当于synchronize,或者是mutex。而condition就好比objectMonitor的方法,或者是操作系统的cond条件变量。

一个lock可以生成多个不同的condition对象,也就是可以有多个不同的条件,对应多个条件队列,能在线程同步中实现更复杂的需求。

public abstract class AbstractQueuedSynchronizer
            extends AbstractOwnableSynchronizer
            implements java.io.Serializable {
    /**
     * 同步队列头节点
     */
    private transient volatile Node head;

    /**
     * 同步队列尾节点
     */
    private transient volatile Node tail;

    /**
     * 同步状态
     */
    private volatile int state;
    
    /**
     * Node节点的实现
     */
    static final class Node {
        //……
    }
    
    /**
     * 位于AQS内部的ConditionObject类,就是Condition的实现
     */
    public class ConditionObject implements Condition, java.io.Serializable {
        private static final long serialVersionUID = 1173984872572414699L;
        /**
         * 条件队列头结点引用
         */
        private transient Node firstWaiter;
        /**
         * 条件队列尾结点引用
         */
        private transient Node lastWaiter;
        
        //……
    }
}

ConditionObject中持有条件队列的头结点引用firstWaiter和尾结点引用lastWaiter。
队列内部的结构也是node节点,和AQS中的同步队列不同的是,条件队列是一个单链表,结点之间使用nextWaiter引用维持后继的关系,并不会用到prev, next属性,它们的值都为null,并且没有哨兵结点,大概结构如下:
在这里插入图片描述
一个lock可以有多个condition对象,一个同步队列,多个条件队列

await

大概步骤为:

  1. 调用addConditionWaiter方法,将当前线程封装成Node.CONDITION类型的Node结点链接到条件队列尾部,返回新加的结点,该过程中将移除取消等待的结点。
  2. 调用fullyRelease方法,内部会调用通用release一次性释放当前线程所占用的所有的锁(重入锁),并返回取消时的同步状态state 值,这个值会在下次线程唤醒的时候重新用来竞争锁。
  3. 循环,调用isOnSyncQueue方法判断结点是否被转移到了同步队列中:
    如果不在同步队列中,那么park挂起当前线程,不在执行后续代码。说明啥,说明条件队列的节点必须被移 动到同步队列。也就是在signal那边肯定有一处代码来进行移动。
  4. 到这一步,结点一定是加入同步队列中了。那么使用acquireQueued自旋获取独占锁,将锁重入次数原封不动的写回去。
/**
 * 位于ConditionObject中的方法
 * 当前线程进入等待状态,直到被通知或中断
 *
 * @throws InterruptedException 如果线程被中断,那么返回并抛出异常
 */
public final void await() throws InterruptedException {
    /*最开始就检查一次,如果当前线程是被中断状态,则清除已中断状态,并抛出异常*/
    if (Thread.interrupted())
        throw new InterruptedException();
    /*当前线程封装成Node.CONDITION类型的Node结点链接到条件队列尾部,返回新加的结点*/
    Node node = addConditionWaiter();
    /*尝试释放当前线程所占用的所有的锁,并保存当前的锁状态*/
    int savedState = fullyRelease(node);
    //中断模式,默认为0 表示没有中断,后面会介绍
    int interruptMode = 0;
/*循环检测,如果当前队列不在同步队列中,那么将当前线程继续挂起,停止执行后续代码,直到被通知/中断;
否则,表示已在同步队列中,直接跳出循环*/
    while (!isOnSyncQueue(node)) {
        //此处线程阻塞
        LockSupport.park(this);
        // 走到这一步说明可能是被其他线程通知唤醒了或者是因为线程中断而被唤醒
        // checkInterruptWhileWaiting检查线程被唤醒的原因,并且使用interruptMode字段记录中断模式
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            //如果是中断状态,则跳出循环,这说明中断状态也会离开条件队列加入同步队列
            break;
        /*如果没有中断,那就是因为signal或者signalAll方法的调用而被唤醒的,并且已经被加入到了同步队列中
         * 在下一次循环时,将不满足循环条件,而自动退出循环*/
    }
    /*
     * 到这一步,结点一定是加入同步队列中了
     * 那么使用acquireQueued自旋获取独占锁,第二个参数就是最开始释放锁时的同步状态,这里要将锁重入次数原封不动的写回去
     * 如果在获取锁的等待过程中被中断,并且之前的中断模式不为THROW_IE(可能是0),那么设置中断模式为REINTERRUPT,
     * 即表示在调用signal或者signalAll方法之后设置的中断状态
     * */
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    /*此时已经获取到了锁,那么实际上这个对应的结点就是head结点了
     *但是如果线程是 在调用signal或者signalAll方法之前就因为中断而被唤醒 的情况时,将结点添加到同步队列的的时候,并没有清除在条件队列中的结点引用
     *因此,判断nextWaiter是否不为null,如果是则还需要从条件队列中移除彻底移除这个结点。
     * */
    if (node.nextWaiter != null)
        //这里直接调用unlinkCancelledWaiters方法移除所有waitStatus不为CONDITION的结点
        unlinkCancelledWaiters();
    //如果中断模式不为0,那么调用reportInterruptAfterWait方法对不同的中断模式做出处理
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

signal

大概步骤如下:

1、检查调用signal方法的线程是否是持有锁的线程,如果不是则直接抛出IllegalMonitorStateException异常。
2、调用doSignal方法将等待时间最长的一个结点从条件队列转移至同步队列尾部,然后根据条件可能会尝试唤醒该结点对应的线程。

/**
 * Conditon中的方法
 * 将等待时间最长的结点移动到同步队列,然后unpark唤醒
 *
 * @throws IllegalMonitorStateException 如果当前调用线程不是获取锁的线程,则抛出异常
 */
public final void signal() {
    /*1 首先调用isHeldExclusively检查当前调用线程是否是持有锁的线程
     * isHeldExclusively方法需要我们重写
     * */
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    //获取头结点
    Node first = firstWaiter;
    /*2 如果不为null,调用doSignal方法将等待时间最长的一个结点从条件队列转移至同步队列尾部,然后根据条件可能会尝试唤醒该结点对应的线程。*/
    if (first != null)
        doSignal(first);
}


/**
 * AQS中的方法
 * 检测当前线程是否是持有独占锁的线程,该方法AQS没有提供实现(抛出UnsupportedOperationException异常)
 * 通常需要我们自己重写,一般重写如下!
 *
 * @return true 是;false 否
 */
protected final boolean isHeldExclusively() {
    //比较获取锁的线程和当前线程
    return getExclusiveOwnerThread() == Thread.currentThread();
}

dosignal

/**
 * Conditon中的方法
 * 从头结点开始向后遍历,从条件队列中移除等待时间最长的结点,并将其加入到同步队列
 * 在此期间会清理一些遍历时遇到的已经取消等待的结点。
 *
 * @param first 条件队列头结点
 */
private void doSignal(Node first) {
    /*从头结点开始向后遍历,唤醒等待时间最长的结点,并清理一些已经取消等待的结点*/
    do {
        //firstWaiter指向first的后继结点,并且如果为null,则lastWaiter也置为null,表示条件队列没有了结点
        if ((firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        //first的后继引用置空,这样就将first出队列了
        first.nextWaiter = null;
        /*循环条件
         * 1 调用transferForSignal转移结点,如果转移失败(结点已经取消等待了);
         * 2 则将first赋值为它的后继,并且如果不为null;
         * 满足上面两个条件,则继续循环
         * */
    } while (!transferForSignal(first) &&
            (first = firstWaiter) != null);
}

transferForSignal会尝试将遍历到的结点转移至同步队列中,调用该方法之前并没有显示的判断结点是不是处于等待状态,而是在该方法中通过CAS的结果来判断。

大概步骤为:

1、尝试CAS将结点等待状态从Node.CONDITION更新为0。这里不存在并发的情况,因为调用线程此时已经获取了独占锁,因此如果更改等待状态失败,那说明该结点原本就不是Node.CONDITION状态,表示结点早已经取消等待了,则直接返回false,表示转移失败。

2、CAS成功,则表示该结点是处于等待状态,那么调用enq将结点添加到同步队列尾部,返回添加结点在同步队列中的前驱结点。

3、获取前驱结点的状态ws。如果ws大于0,则表示前驱已经被取消了或者将ws改为Node.SIGNAL失败,表示前驱可能在此期间被取消了,那么调用unpark方法唤醒被转移结点中的线程,好让它从await中的等待中醒来;否则,那就由它的前驱结点在获取锁之后释放锁时再唤醒。返回true。

/**
 * 将结点从条件队列转移到同步队列,并尝试唤醒
 *
 * @param node 被转移的结点
 * @return 如果成功转移,返回true;失败则返回false
 */
final boolean transferForSignal(Node node) {
    /*1 尝试将结点的等待状态变成0,表示取消等待
    如果更改等待状态失败,那说明一定是原本就不是Node.CONDITION状态,表示结点早已经取消等待了,则返回false。
    这里不存在并发的情况,因为调用线程此时已经获取了独占锁*/
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    /*2 将结点添加到同步队列尾部,返回添加结点的前驱结点*/
    Node p = enq(node);
    //获取前驱结点的状态ws
    int ws = p.waitStatus;
    /*3 如果ws大于0 表示前驱已经被取消了 或者 将ws改为Node.SIGNAL失败,表示前驱可能在此期间被取消了
    则调用unpark方法唤醒被转移结点中的线程,好让它从await中的等待唤醒(后续尝试获取锁)
    否则,那就由它的前驱结点获取锁之后释放锁时再唤醒。
    */
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    //返回true
    return true;
}

总结

条件队列中,很多操作都不需要用到cas,是因为条件队列await和signal的前提,就是线程已经获取到了锁,所以不会有很多的并发问题。
在这里插入图片描述

参考

https://www.yuque.com/snab/java/agbmupgrg4159wx7#ekKlb

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

多线程进阶学习10------AQS详解 的相关文章

随机推荐

  • Go依赖管理及Go module使用

    目录 一 依赖管理 二 为什么需要依赖管理 三 godep 3 1 安装 3 2 基本命令 3 3 使用godep 3 4 vender机制 3 5 godep开发流程 四 go module 4 1 GO111MODULE 4 2 GOP
  • 周杰伦演唱会总是抢不到票?教你用Python做一个自动抢票脚本

    相信想去周董演唱会的大家都用过大麦网抢票吧 可是 抢不到啊 该说不说 我抢到了 那么 今天带大家用Python来制作一个自动抢票的脚本小程序 知识点 面向对象编程 selenium 操作浏览器 pickle 保存和读取Cookie实现免登陆
  • 微信公众号分享功能 errMsg: “config:fail,Error: 系统错误,错误码:63002,invalid signature [20210130 10:16:32][]“

    H5 分享 请求分享页面时 系统错误 errMsg config fail Error 系统错误 错误码 63002 invalid signature 20210130 10 16 32 解决方案 报错信息 签名错误 需要检查自己的签名是
  • 反转数组(两种方法)

    a 第一种方法 创建一个新数组接收并返回 var arr1 1 2 3 var arr2 reverse1 arr1 console log arr2 console log reverse1 arr1 定义一个新数组 把老数组中的元素反向
  • UsbWebserver + MySQL + Apache的和环境配置问题(主要是连不上MySQL的问题)

    问题1 Apche感叹号 是因为软件路径中含有中文 可以把文件转移到没中文的路径下 问题2 mysqli connect Server sent charset 255 unknown to the client php集成环境 usbwe
  • 电子闹钟设计

    摘要 本设计是以89c51单片机作为控制核心的闹铃系统 本文大致可以分三个章节 第一章讲用单片机制作电子闹钟带来的优势 还有电子闹钟在未来的电子产品中的趋势以及本次设计所要实现的课题目标 第二章讲一些设计思路和硬件组成 第三讲程序代码 最后
  • CVE10大漏洞总结【网络安全】

    1 OpenSSL心脏出血漏洞 漏洞描述 这项严重缺陷 CVE 2014 0160 的产生是由于未能在memcpy 调用受害用户输入内容作为长度参数之前正确进行边界检查 攻击者可以追踪OpenSSL所分配的64KB缓存将超出必要范围的字节信
  • std::string::replace使用小计

    ctrl c ctrl v 真是害死人 看起来简单的东西 往往却又很容易出问题 究其原因 还在于只知其表不知其里 容易想当然 今天遇到的问题是简单调用string replace替换子串的问题 如 std string sTest1 12x
  • web服务器开发课程项目实训,Web前端开发实训案例教程(初级)

    目 录 第1章 实践概述 1 1 1 实践目标 1 1 2 实践知识地图 1 1 3 实施安排 6 1 3 1 实验部分 技术专题 6 1 3 2 综合实践部分 11 第2章 网页设计与制作 19 2 1 实验目标 19 2 2 实验任务
  • FTP命令详解

    FTP命令是Internet用户使用最频繁的命令之一 不论是在DOS还是UNIX操作系统下使用FTP 都会遇到大量的FTP内部命令 熟悉并灵活应用FTP的内部命令 可以大大方便使用者 并收到事半功倍之效 FTP的命令行格式为 ftp v d
  • bytebuffer 使用demo

    pom文件
  • 微信小程序路由

    wx reLaunch Object object 关闭所有页面 打开到应用内的某个页面 一般是跳转到首页使用 例 wx reLaunch url url wx navigateTo Object object 保留当前页面 跳转到应用内的
  • Java时间转换问题 [Failed to convert property value of type ‘java.lang.String‘ to required type ‘java.

    1 错误提示代码 default message Failed to convert property value of type java lang String to required type java 2 分析原因 遇到java接收
  • macOS 系统下安装Lua及lua-cjson

    macOS 系统下安装Lua及lua cjson lua安装及部署 具体操作步骤如下 curl R O http www lua org ftp lua 5 2 3 tar gz tar zxf lua 5 2 3 tar gz cd lu
  • 豆瓣读书top250数据爬取与可视化

    爬虫 scrapy 题目 根据豆瓣读书top250 根据出版社对书籍数量分类 绘制饼图 搭建环境 import scrapy import numpy as np import pandas as pd import matplotlib
  • UE5《Electric Dreams》项目PCG技术解析 之 PCGCustomNodes详解(三)SG_CopyPointsWithHierarchy

    继续解析 Electric Dreams 项目中的自定义节点和子图 SG CopyPointsWithHierarchy和PostCopyPoints OffsetIndices 文章目录 前导文章 标准组合拳 SG CopyPointsW
  • STM32开发中各库函数的主要作用和关系。

    STM32开发中各库函数的主要作用和关系 STM32各库函数关系的简单解析 您好 这是我第一次使用 CSDN来发布文章 如果有排版不合理 结构凌乱 欢迎私信我交流经验 文章内容如有错误 欢迎读者指正 首先我们了解一下什么是库函数 众所周知
  • 常见的几种开源协议

    在学习中经常能看到一些词 例如 GPL LGPL等等 自打上学那会就遇见过 对它们的具体含义却不了解 今天给它们总结一下 说到开源协议 不得不提GNU 课本上给的定义是 GNU is Not Unix 这是官方给出的递归定义 永远也找不到本
  • Linux基础服务3——samba

    文章目录 一 基本了解 1 1 服务安装 1 2 服务进程和端口 1 3 samba用户 1 4 配置文件 1 4 1 主配置文件 1 4 2 配置文件参数 1 5 安全级别 二 访问samba 2 1 参数测试 2 2 交互式访问 2 3
  • 多线程进阶学习10------AQS详解

    AbstractQueuedSynchronizer 来自于JDK1 5 位于JUC包 由并发编程大师Doug Lea编写 字面翻译就是 抽象队列同步器 简称为AQS AQS作为一个抽象类 是构建JUC包中的锁 比如ReentrantLoc