文章目录

  • 什么是AQS
  • AQS做了什么
  • AQS是如何保证并发安全的
  • 父类 AbstractOwnableSynchronizer
  • 主要成员变量
  • CLH队列
  • 静态内部类Node
    • waitStatus取值常量
    • 锁类型常量
    • CAS操作相关的常量
  • 独占锁的获取与释放流程
    • 获取独占锁 acquire()
      • tryAcquire()
      • addWaiter()
      • acquireQueued()
    • 释放独占锁release()
      • tryRelease()
      • unparkSuccessor()
  • 共享锁的获取与释放流程
    • 获取共享锁acquireShared()
      • tryAcquireShared()
      • doAcquireShared()
    • 释放共享锁releaseShared()
      • tryReleaseShared()
      • doReleaseShared()
  • 内部类 ConditionObject
    • Condition实现主要逻辑
    • 成员变量
    • 线程中断模式常量
    • 核心方法
      • await() 加入条件等待
      • signal()通知线程结束等待
      • signalAll()

什么是AQS

全称 AbstractQueuedSynchronizer,意为抽象的队列同步器

顾名思义它是一个抽象类,内部存在一个同步队列,在存在并发安全的地方使多线程同步执行

AQS做了什么

AQS 的实现使用模板方法设计模式,其内设计了线程从获取锁到到释放锁的整个流程,并实现了获取失败加入队列、释放锁后唤醒等待线程以及条件等待 condition 等大部分功能,子类只需实现其预留的钩子方法即可,像java中的重入锁 ReentrantLock、读写锁 ReentrantReadWriteLock 以及常用的并发工具类 CountDownLatchSemaphore 等都是利用AQS实现的

AQS是如何保证并发安全的

AQS 通过volatile 加 上 CAS 来保证并发安全

volatile
volatile 修饰的变量在被线程修改后会立即刷新到主内存中,并使其他线程对该变量的缓存失效,使其重新从主内存中读取,这样就解决了多线程并发可见性问题
volatile 通过设置内存屏障来禁止JVM指令重排序, 从而解决多线程并发有序性

CAS
Compare and Swap,即比较再交换

线程会把变量在主内存中的值拷贝到自己的工作空间,更新这个变量时,会比较工作空间的值是否与主内存中的值一样,一样则更新,否则不更新。这样能解决多线程并发的原子性问题
它通过volatile(解决可见性和有序性问题)加 CAS(解决原子性问题)来保证并发安全

父类 AbstractOwnableSynchronizer

这个类的实现很简单,就是提供设置(获取)持有独占锁线程方法

 private transient Thread exclusiveOwnerThread;protected final void setExclusiveOwnerThread(Thread thread) {exclusiveOwnerThread = thread;}protected final Thread getExclusiveOwnerThread() {return exclusiveOwnerThread;}

主要成员变量

private volatile int state;//锁计数器,为0表示无锁状态
private transient volatile Node head; // CLH对列对头
private transient volatile Node tail; // CLH队列队尾

CLH队列

一个虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系,下文中的同步队列指的就是这个队列

静态内部类Node

Node是同步队列的结点类,主要成员变量如下:

volatile int waitStatus; //等待状态, 下文中的ws就是指这个变量
volatile Node prev; //前驱结点
volatile Node next; //后继结点
volatile Thread thread; //等待的线程
Node nextWaiter; //使用condition时构成的单向条件队列,指向下一个等待结点

除了 nextWaiter 其他变量都用 volatile 修饰保证多线程环境下可见性,而每个线程结点的 nextWaiter 都是不一样的,相当于线程的局部变量,所以不需要 volatile 修饰

waitStatus取值常量

static final int CANCELLED =  1;  //表示线程已经被取消,这种结点会被移出队列
static final int SIGNAL    = -1;  //表示当前线程释放锁后需要唤醒后继节点
static final int CONDITION = -2;  //表示当前结点在等待一个condition条件队列中
static final int PROPAGATE = -3; //表示共享锁模式下,当前结点以传播的方式唤醒结点,(也就是唤醒的下个结点后还会继续唤醒其他结点)

waitStatus还有个取值为0,相当于默认值,AQS 中有多处会将waitStatus设置为0

锁类型常量

static final Node SHARED = new Node();  //共享锁模式
static final Node EXCLUSIVE = null;    //独占锁模式

CAS操作相关的常量

 private static final Unsafe unsafe = Unsafe.getUnsafe(); //提供获取变量地址偏移量和cas操作方法的工具类,其内部都是一些native方法private static final long stateOffset; //成员变量state的地址偏移量private static final long headOffset; //成员变量head的地址偏移量private static final long tailOffset; //成员变量tail的地址偏移量private static final long waitStatusOffset; //成员变量waitStatus的地址偏移量private static final long nextOffset; //成员变量next的地址偏移量//这些偏移地址常量在对变量进行CAS操作时使用,通过静态代码块进行初始化static {try {stateOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("state"));headOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("head"));tailOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("tail"));waitStatusOffset = unsafe.objectFieldOffset(Node.class.getDeclaredField("waitStatus"));nextOffset = unsafe.objectFieldOffset(Node.class.getDeclaredField("next"));} catch (Exception ex) { throw new Error(ex); }}

CAS操作相关的方法

    /*** @param expect 期望值* @param update 更新值*/protected final boolean compareAndSetState(int expect, int update) {//stateOffset state的地址偏移量return unsafe.compareAndSwapInt(this, stateOffset, expect, update);}

类似的方法还有compareAndSetHead、compareAndSetTail、compareAndSetWaitStatus、compareAndSetNext

独占锁的获取与释放流程

获取独占锁 acquire()

 /*** @param arg 获取锁,计数器state需要加arg,一般取值为1*/public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))/*** 尝试获取锁失败,则执行acquireQueued()加入队列,并将当前线程挂起*  线程重新唤醒后acquireQueued()返回线程是否被标记中断,若是则中断线程*/selfInterrupt();}

tryAcquire()

空实现,留给子类实现的钩子方法,真正获取锁的逻辑就在这个方法中

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

下面是 ReentrantLock 非公平锁的实现,主要逻辑是判断state为0或是当前线程持有独占锁,则以 CAS 方式给state加上acquires(一般都是1)成功则获取锁,否则获取锁失败

        protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);}
`       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) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}

addWaiter()

线程加入同步队列

 /*** @param mode 锁类型,对应类型常量SHARED(共享锁)、EXCLUSIVE(独占锁)*/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()入队,失败会不断重试直至成功enq(node);return node;}

enq() 线程结点入队

    private Node enq(final Node node) {for (;;) { //操控失败会重试,直至成功才跳出循环Node t = tail;if (t == null) { //tail为null,head也一定为null,对列初始化/*** 这里先设置head再把head赋值给tail,而不是先设置tail再把tail赋值给head的原因.* 假如执行compareAndSetHead后如果当前线程A失去cpu执行权,线程B再进入此处执行compareAndSetHead,* 因为compareAndSetHead方法中设置head时,期望值为null,而线程A已经执行compareAndSetHead给head设值了,* 所以线程B执行compareAndSetHead会一直失败,直到线程A重新获得cpu执行权给tail赋值,* 再轮到线程B执行时,tail不为null直接执行else处代码,这样防止了多个线程给head和tail设值而导致出现线程安全问题*/if (compareAndSetHead(new Node()))tail = head;} else {node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;return t;}}}}private final boolean compareAndSetHead(Node update) {//期望值为nullreturn unsafe.compareAndSwapObject(this, headOffset, null, update);}

acquireQueued()

挂起线程等待,唤醒后重新去获取锁

    final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (;;) {final Node p = node.predecessor();//获取锁成功,head结点出队,当前结点变为headif (p == head && tryAcquire(arg)) { setHead(node); p.next = null; failed = false;return interrupted;}//获取锁失败,挂起线程,等待重新唤醒后再执行后续代码if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}}

shouldParkAfterFailedAcquire()

若前驱结点的ws为SIGNAL返回true,
若为CANCELLED,则清理同步队列中ws为CANCELLE结点,在acquireQueued()则会重新获取node的前驱结点再继续调用该方法
若ws 为其他值则将ws设为SIGNAL

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {int ws = pred.waitStatus;if (ws == Node.SIGNAL)return true;if (ws > 0) {// 将ws为CANCELLED移出队列do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else {compareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;

parkAndCheckInterrupt() 挂起线程

    private final boolean parkAndCheckInterrupt() {LockSupport.park(this); // 内部调用了Unsafe的native方法//线程再次唤醒后,返回线程是否被中断return Thread.interrupted();}

释放独占锁release()

    public final boolean release(int arg) {if (tryRelease(arg)) {Node h = head;//释放锁成功且head不为0则唤醒后继结点if (h != null && h.waitStatus != 0)unparkSuccessor(h);return true;}return false;}

tryRelease()

释放锁的主要逻辑,交给子类实现的钩子方法

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

下面是 ReentrantLock中的实现,主要逻辑:持有锁的线程为当前线程时,将state的值减去releases

        protected final boolean tryRelease(int releases) {int c = getState() - releases;if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;if (c == 0) {free = true;setExclusiveOwnerThread(null);}// 持有独占锁的线程只有一个,这段代码时单线程执行的,不需要使用cas的方式设置statesetState(c); return free;}

unparkSuccessor()

唤醒后继结点线程

    private void unparkSuccessor(Node node) {int ws = node.waitStatus;if (ws < 0)compareAndSetWaitStatus(node, ws, 0);Node s = node.next;if (s == null || s.waitStatus > 0) {//后继结点为空或者ws为CANCELLED,从tail开始寻找到一个不为null且ws不为CANCELLED的结点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); //唤醒线程,内部调用UNSAFE类的本地方法}

共享锁的获取与释放流程

获取共享锁acquireShared()

    public final void acquireShared(int arg) {if (tryAcquireShared(arg) < 0)// 获取锁失败加入同步队列doAcquireShared(arg);}

tryAcquireShared()

获取锁的主要逻辑方法,也是交给子类实现的钩子方法

    /*** 返回负数表示获取锁失败,* 返回0表示成功且没有线程在等待获取共享锁* 返回正数也表示成功但有线程在等待获取共享锁*/protected int tryAcquireShared(int arg) {throw new UnsupportedOperationException();}

下面是 ReentrantReadWriteLock中的实现,
主要逻辑是判断独占锁有没有被其他线程持有,
若没有,则通过CAS操作修改state成功则获取到锁,失败也会进入循环中不断重试
若有,则获取锁失败

    protected final int tryAcquireShared(int unused) {Thread current = Thread.currentThread();int c = getState();if (exclusiveCount(c) != 0 &&getExclusiveOwnerThread() != current) // 独占锁被其他线程持有不能获取共享锁直接失败,return -1;int r = sharedCount(c);if (!readerShouldBlock() && // 是否应该阻塞,对于公平锁来说前面有线程等待则应该被阻塞r < MAX_COUNT &&compareAndSetState(c, c + SHARED_UNIT)) {// cas操作成功则获取到锁if (r == 0) {firstReader = current;firstReaderHoldCount = 1;} else if (firstReader == current) {firstReaderHoldCount++;} else {HoldCounter rh = cachedHoldCounter;if (rh == null || rh.tid != getThreadId(current))cachedHoldCounter = rh = readHolds.get();else if (rh.count == 0)readHolds.set(rh);rh.count++;}return 1;  // 获取锁成功返回正数1}/*** 进入循环中重试,* 只要独占锁没有被持,共享锁可以多个线程持有的,* 上面获锁失败可能是cas操作失败(其他获取共享锁的线程也会并发修改state)* 所以要进入循环中重试*/return fullTryAcquireShared(current); }

doAcquireShared()

加入同步队列中等待,并将线程挂起

    private void doAcquireShared(int arg) {final Node node = addWaiter(Node.SHARED); //构建一个当前结点共享模式的nodeboolean failed = true;try {boolean interrupted = false;for (;;) {final Node p = node.predecessor();if (p == head) {int r = tryAcquireShared(arg);// 成功获取共享锁if (r >= 0) {//设置当前结点为head并并以传播的方式唤醒等待共享锁的线程setHeadAndPropagate(node, r); p.next = null; // help GCif (interrupted)selfInterrupt();failed = false;return;}}if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt()) // 设置前驱结点ws为SIGNAL并挂起线程interrupted = true;}} finally {if (failed)cancelAcquire(node);}}

setHeadAndPropagate()

将当前结点设置为head,如果后继结点在等待共享锁则需要将其唤醒
获取共享锁的线程之间是不冲突的,一个线程获取到共享锁,其他等待的线程也应被唤醒获取共享锁

    private void setHeadAndPropagate(Node node, int propagate) {Node h = head; setHead(node);/*** propagate>0说明有线程等待共享锁* 老的head或当前最新的head的ws不为CANCELLED(小于0就是不为CANCELLED)* 这些情况都说明有线程在等待锁,* 这时获取node的后继结点如果为共享锁模式则唤醒后继结点*/if (propagate > 0 || h == null || h.waitStatus < 0 ||(h = head) == null || h.waitStatus < 0) {Node s = node.next;if (s == null || s.isShared())doReleaseShared(); // 唤醒队列中等待共享锁的线程,这个方法在释放锁处会详细讲到}

释放共享锁releaseShared()

    public final boolean releaseShared(int arg) {if (tryReleaseShared(arg)) {doReleaseShared(); //释放锁成功,唤醒等待线程return true;}return false;}

tryReleaseShared()

释放锁的主要逻辑方法,交给子类实现的钩子方法

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

下面是 ReentrantReadWriteLock中的实现,
主要逻辑也是修改state值,但与与独占锁不同,这里存在多线程并发,所以要以CAS方式修改,并且失败需要不断重试

    protected final boolean tryReleaseShared(int unused) {Thread current = Thread.currentThread();if (firstReader == current) {if (firstReaderHoldCount == 1)firstReader = null;elsefirstReaderHoldCount--;} else {HoldCounter rh = cachedHoldCounter;if (rh == null || rh.tid != getThreadId(current))rh = readHolds.get();int count = rh.count;if (count <= 1) {readHolds.remove();if (count <= 0)throw unmatchedUnlockException();}--rh.count;}for (;;) {int c = getState();int nextc = c - SHARED_UNIT;if (compareAndSetState(c, nextc)) // cas方式修改statereturn nextc == 0;}}

doReleaseShared()

        for (;;) {Node h = head;if (h != null && h != tail) {int ws = h.waitStatus;if (ws == Node.SIGNAL) {if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) // 这里cas操作成功后再去唤醒后继线程防止多个线程同时唤醒同一个线程continue;            unparkSuccessor(h);}else if (ws == 0 &&!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))/*** ws为0说明h的后继结点已经或正在被唤醒,这时将h标记为PROPAGATE状态* cas操作失败说明,第一个判断后线程失去执行权,* 其他线程已经将h的ws设为了PROPAGATE,* 说明这时多线程竞争激烈(有很多等待共享锁的线程被唤醒,也就可能还有很多线程没被唤醒),* 这时当前线程就继续进入循环与其他线程一起唤醒等待的线程*/continue;  }/*** head结点改变,说明在当前线程执行一次循环中,有其他线程唤醒了等待线程并重置了head,*  这时同样说明多线程竞争激烈,当前线程就继续进入循环来唤醒其他线程* 同时这也是退出循环的标志,因为head的后继结点为最后一个要唤醒的结点时,* 其他线程就没有结点要唤醒了,这是head就不会被其他线程重置,h也就一定等于head* 当然h等于head并不说明一定是最后一个结点被唤醒,但说明多线程竞争并不激烈,也就不需要帮助其他线程一起唤醒等待的线程*/if (h == head)   //head改变,继续进入循环继续队列中的线程break;}

唤醒等待共享锁线程是并发进行的,因为不止释放共享锁时会唤醒等待的线程,线程在被唤醒并获取到共享锁后,也会唤醒其他等待共享锁的线程,而被唤醒的线程获取到锁后也会加入到这个队伍去唤醒其他等待的线程,这样即使有大量等待线程,也会很快被全部唤醒

内部类 ConditionObject

ConditionObject类AQS是实现Condition功能的一个内部类

Condition实现主要逻辑

await()会将当前线程A加入条件队列,然后释放锁并挂起线程A,
当其他线程B调用signal()和signalAll()并不会将挂起的线程A唤醒,而是将线程A移出条件队列并移入同步队列,
等到持有锁的线程C释放锁后才会被唤醒,唤醒后的线程A还要去获取锁若不成功的话还要被挂起

成员变量

     /** 条件队列中的第一个等待的结点 */private transient Node firstWaiter;/** 条件队列中的最后一个等待的结点 */private transient Node lastWaiter;

线程中断模式常量

        /** 调用thread interrupt()中断 */private static final int REINTERRUPT =  1;/** 抛异常中断 throw new InterruptedException() */private static final int THROW_IE    = -1;

核心方法

await() 加入条件等待

        public final void await() throws InterruptedException {if (Thread.interrupted())throw new InterruptedException();Node node = addConditionWaiter(); //线程加入条件队列int savedState = fullyRelease(node); //释放锁资源并返回state在释放锁之前的值int interruptMode = 0;while (!isOnSyncQueue(node)) {/*** 当前线程不在队列中,挂起线程* 当其他线程调用signal通知到了该线程会将这个线程加入同步队列中* 再轮到该线程执行时就可以跳出循环*/LockSupport.park(this);//线程挂起等待时被中断跳出循环if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)break;}//重新获取锁,失败则线程又会被挂起if (acquireQueued(node, savedState) && interruptMode != THROW_IE)interruptMode = REINTERRUPT;if (node.nextWaiter != null) // clean up if cancelledunlinkCancelledWaiters();if (interruptMode != 0)reportInterruptAfterWait(interruptMode);}

addConditionWaiter() 加入条件队列

        private Node addConditionWaiter() {Node t = lastWaiter;// If lastWaiter is cancelled, clean out.if (t != null && t.waitStatus != Node.CONDITION) {unlinkCancelledWaiters(); //这里将条件队列中所有ws不为CONDITION的结点清除t = lastWaiter;}Node node = new Node(Thread.currentThread(), Node.CONDITION);if (t == null)firstWaiter = node;elset.nextWaiter = node;lastWaiter = node;return node;}

fullyRelease() 释放锁资源

    final int fullyRelease(Node node) {boolean failed = true;try {int savedState = getState();if (release(savedState)) {failed = false;return savedState;} else {throw new IllegalMonitorStateException();}} finally {if (failed)node.waitStatus = Node.CANCELLED;}}

isOnSyncQueue() 结点是否在同步队列中

    final boolean isOnSyncQueue(Node node) {if (node.waitStatus == Node.CONDITION || node.prev == null)return false;if (node.next != null) // If has successor, it must be on queuereturn true;return findNodeFromTail(node); //在同步队列中查找存在node结点}private boolean findNodeFromTail(Node node) {Node t = tail;for (;;) { //从后往前遍历寻找是否存在nodeif (t == node)return true;if (t == null)return false;t = t.prev;}}

signal()通知线程结束等待

        public final void signal() {if (!isHeldExclusively()) //当前线程是否占有独占锁throw new IllegalMonitorStateException();Node first = firstWaiter;if (first != null)doSignal(first); //通知条件队列中第一个等待线程}

doSignal()

        private void doSignal(Node first) {do {if ( (firstWaiter = first.nextWaiter) == null) //这里将first移出条件队列lastWaiter = null;first.nextWaiter = null;} while (!transferForSignal(first) && //将first加入同步队列(first = firstWaiter) != null);}

signalAll()

        public final void signalAll() {if (!isHeldExclusively())throw new IllegalMonitorStateException();Node first = firstWaiter;if (first != null)doSignalAll(first); //条件队列所有结点全部出队}

doSignalAll()

        private void doSignalAll(Node first) {lastWaiter = firstWaiter = null;do {Node next = first.nextWaiter;first.nextWaiter = null;transferForSignal(first);first = next;} while (first != null);}

大厂高频面试题之Java并发核心AQS源码详解相关推荐

  1. java的String类源码详解

    java的String类源码详解 类的定义 public final class Stringimplements java.io.Serializable, Comparable<String ...

  2. 【java】LinkedList1.8源码详解

    目录 前言 概要 属性 构造方法 核心方法 get(int index) set(int index, E element) add(int index, E element) addAll(Coll ...

  3. Java并发编程-AQS源码之条件队列

    System.out.println(name + "==>成功获取到锁" + lock); try { condition.await(); } catch (Interr ...

  4. 你不可不知的Java引用类型之——SoftReference源码详解

    定义 SoftReference是软引用,其引用的对象在内存不足的时候会被回收.只有软引用指向的对象称为软可达(softly-reachable)对象. 说明 垃圾回收器会在内存不足,经过一次垃圾回收 ...

  5. XxlJob(一) 分布式定时任务XxlJob用法及核心调度源码详解

    目录 一.XxlJob 的Executor 1. 使用Spring框架注入 2. 不使用框架注入 3. 使用jar包的形式集成executor 二.XxlJob的核心工作原理 1. 注册JobHand ...

  6. 【课程设计】Java 计算器实现(源码 + 详解)

  7. 并发编程五:java并发线程池底层原理详解和源码分析

    文章目录 java并发线程池底层原理详解和源码分析 线程和线程池性能对比 Executors创建的三种线程池分析 自定义线程池分析 线程池源码分析 继承关系 ThreadPoolExecutor源码分 ...

  8. 【JAVA秘籍心法篇-Spring】Spring XML解析源码详解

    [JAVA秘籍心法篇-Spring]Spring XML解析源码详解 所谓天下武功,无坚不摧,唯快不破.但有又太极拳法以快制慢,以柔克刚.武功外式有拳打脚踢,刀剑棍棒,又有内功易筋经九阳神功.所有外功 ...

  9. java的数组与Arrays类源码详解

    java的数组与Arrays类源码详解 java.util.Arrays 类是 JDK 提供的一个工具类,用来处理数组的各种方法,而且每个方法基本上都是静态方法,能直接通过类名Arrays调用. 类的 ...

最新文章

  1. websocket并发性测试
  2. flexpaper 背景色变化
  3. linux脚本石英钟,原生JS实现的简单小钟表功能示例
  4. Excel vba引用工作表的三种写法
  5. ASA SSL ××× Anyconnect SBL(Start Before Logon)用于在外网登录域(上)
  6. 项目中的模块剥离成项目_使用MCEBuddy 2从电视录制中剥离广告
  7. 编程实现newton插值c++_数据体操:数据处理和IDW地理插值算法
  8. java 断点续传 开源_java断点续传后台代码
  9. python的安全插件
  10. tp3.2 模型实例化
  11. 推荐这5款Windows软件,一款比一款惊喜
  12. Lights off(关灯游戏)终极算法
  13. python调用有道翻译_python调用有道云翻译api
  14. java候选码计算的替换法_数据库闭包和候选码求解方法
  15. 手把手教你做游戏外挂
  16. lateral view 和 lateral view outer的区别
  17. 【SpringBoot项目中使用Mybatis批量插入百万条数据】
  18. python图形用户界面page_Python+selenium使用PageObject实现UI自动化
  19. 开酒馆前的注意事项 (上)
  20. OpenCV-Python学习(18)—— OpenCV 图像几何变换之图像平移(cv.warpAffine)

热门文章

  1. firefox下载网站图片
  2. 什么是淘宝关键词以及如何查找关键词的方法
  3. cygwin环境下ffmpeg中av_register_all()函数,警告过时问题。
  4. 分布式系统领域有哪些经典论文
  5. 计算机管理-设备管理器没有找到打印机,win7系统设备管理器没有ieee1284.4设备的解决方法...
  6. 3dmax学习资料记录
  7. 程序员——伤不起的三十岁
  8. matlab在sin处出现解析错误,用matlab function时出现一些错误,看不太懂
  9. 奔驰c语言控制系统使用方法,奔驰主动车身控制ABC系统技术资料(三)
  10. 关于python在64位机器上打包32位exe(兼容xp系统)解决方法