2019独角兽企业重金招聘Python工程师标准>>>

原理剖析(第 009 篇)ReentrantReadWriteLock工作原理分析

一、大致介绍

1、在前面章节了解了AQS和Semaphore后,想必大家已经对获取独占锁、获取共享锁有了一定的了解了;
2、而JDK中有一个关于读锁写锁分离的工具类,读锁是共享锁,写锁是排他锁,也是基于AQS实现的;
3、那么本章节就和大家分享分析一下JDK1.8的ReentrantReadWriteLock的工作原理; 

二、简单认识ReentrantReadWriteLock

2.1 何为ReentrantReadWriteLock?

1、ReentrantReadWriteLock从英文字面上可理解为可重入的读写锁;2、ReentrantReadWriteLock具备读锁与写锁,读锁是共享锁使用共享模式,写锁是排它锁使用独占模式;3、ReentrantReadWriteLock具备公平与非公平策略,"读-写"互斥、"写-写"互斥;

2.2 ReentrantReadWriteLock的state关键词

1、ReentrantReadWriteLock的state关键字,有点像ThreadPoolExecutor工作线程数量值ctl的味道;2、ReentrantReadWriteLock高16位为读锁的计数值,低16位为写锁的计数值;

2.3 常用重要的成员属性

1、private final ReentrantReadWriteLock.ReadLock readerLock;// 读锁对象2、private final ReentrantReadWriteLock.WriteLock writerLock;// 写锁对象3、final Sync sync;// 同步器4、static final int SHARED_SHIFT   = 16; // 分界线偏移值,用来向左或向右偏移尾数,以此来获取读写锁计数值static final int SHARED_UNIT    = (1 << SHARED_SHIFT); // 读锁需要加1时递增的增量static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1; // 读锁、写锁的最大计数值数量static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; // 写锁掩码,用于写锁计数值的低16位有效值5、private transient ThreadLocalHoldCounter readHolds;// 保存当前线程重入读锁次数的容器,当读锁重入次数为0时则会被移除掉6、private transient HoldCounter cachedHoldCounter;// 最近一个成功获取读锁的线程的计数对象

2.4 常用重要的方法

1、public ReentrantReadWriteLock()// 创建一个读写锁的对象,默认的策略是非公平策略2、public ReentrantReadWriteLock(boolean fair)// 创建一个读写锁的对象,且是否公平方式由传入的fair布尔参数值决定3、public ReentrantReadWriteLock.WriteLock writeLock()// 获取写锁对象4、public ReentrantReadWriteLock.ReadLock  readLock()// 获取读锁对象5、public final boolean isFair() // 查看当前的读写锁对象用的策略方式是啥,是公平策略,还是非公平策略6、protected Thread getOwner()// 获取持有独占锁的线程对象7、public int getReadLockCount()// 获取持有读锁计数值8、public boolean isWriteLocked()// 查看是否有线程持有写锁9、public boolean isWriteLockedByCurrentThread()// 查看当前的线程是不是持有独占写锁的线程10、public int getWriteHoldCount()// 获取当前线程在此写锁上保持的重入锁数量11、public int getReadHoldCount()// 获取当前线程在此读锁上保持的重入锁数量12、protected Collection<Thread> getQueuedWriterThreads()// 返回一个 collection,它包含可能正在等待获取写入锁的线程13、protected Collection<Thread> getQueuedReaderThreads()// 返回一个 collection,它包含可能正在等待获取读取锁的线程14、public final boolean hasQueuedThreads()// 查看是否有阻塞的线程队列15、public final boolean hasQueuedThread(Thread thread)// 查询给定的线程是否正处于阻塞队列中16、abstract boolean readerShouldBlock();// 抽象方法,由AQS的子类实现,读锁是否需要阻塞17、abstract boolean writerShouldBlock();// 抽象方法,由AQS的子类实现,写锁是否需要阻塞18、static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }// 获取读锁的计数值19、static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }// 获取写锁的计数值

2.5 设计与实现伪代码

1、获取写锁:public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}acquire{如果尝试获取独占锁失败的话( 尝试获取独占锁的各种方式由AQS的子类实现 ),那么就新增独占锁结点通过自旋操作加入到队列中,并且根据结点中的waitStatus来决定是否调用LockSupport.park进行休息}2、释放写锁:public final boolean release(int arg) {if (tryRelease(arg)) {Node h = head;if (h != null && h.waitStatus != 0)unparkSuccessor(h);return true;}return false;}release{如果尝试释放独占锁成功的话( 尝试释放独占锁的各种方式由AQS的子类实现 ),那么取出头结点并根据结点waitStatus来决定是否有义务唤醒其后继结点}3、获取读锁:public final void acquireShared(int arg) {if (tryAcquireShared(arg) < 0)doAcquireShared(arg);}acquireShared{如果尝试获取共享锁失败的话( 尝试获取共享锁的各种方式由AQS的子类实现 ),那么新增共享锁结点通过自旋操作加入到队尾中,并且根据结点中的waitStatus来决定是否调用LockSupport.park进行休息}4、释放读锁:public final boolean releaseShared(int arg) {if (tryReleaseShared(arg)) {doReleaseShared();return true;}return false;}releaseShared{如果尝试释放共享锁失败的话( 尝试释放共享锁的各种方式由AQS的子类实现 ),那么通过自旋操作唤完成阻塞线程的唤起操作}

三、源码分析ReentrantReadWriteLock

3.1、Sync同步器

1、AQS --> Sync ---> FairSync // 公平策略||> NonfairSync // 非公平策略2、ReentrantReadWriteLock内的同步器都是通过Sync抽象接口来操作调用关系的,细看会发现基本上都是通过sync.xxx之类的这种调用方式的;

3.2、ReentrantReadWriteLock构造器

1、构造器源码:// 构造方法一:/*** Creates a new {@code ReentrantReadWriteLock} with* default (nonfair) ordering properties.*/public ReentrantReadWriteLock() {this(false);}// 构造方法二:/*** Creates a new {@code ReentrantReadWriteLock} with* the given fairness policy.** @param fair {@code true} if this lock should use a fair ordering policy*/public ReentrantReadWriteLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();readerLock = new ReadLock(this);writerLock = new WriteLock(this);}  2、xxxxxxxxxxxxxxxxxxx

3.3、tryAcquire(int)

1、源码:// ReentrantReadWriteLock 的静态内部类 Sync 的 tryAcquire 方法,写锁获取锁的核心方法protected final boolean tryAcquire(int acquires) {/** Walkthrough:* 1. If read count nonzero or write count nonzero*    and owner is a different thread, fail.* 2. If count would saturate, fail. (This can only*    happen if count is already nonzero.)* 3. Otherwise, this thread is eligible for lock if*    it is either a reentrant acquire or*    queue policy allows it. If so, update state*    and set owner.*/Thread current = Thread.currentThread(); // 获取当前的线程对象int c = getState(); // 获取最新的读写锁资源值int w = exclusiveCount(c); // 然后查看独占锁的占有线程数量if (c != 0) { // c不为零,说明有线程占有写锁// (Note: if c != 0 and w == 0 then shared count != 0)// 如果w=0,说明已经有线程占有了读锁,那么当前想获取写锁的话没必要了,直接返回false到队列中排队去// 如果w!=0,说明已经有线程占有了写锁,那么再看看当前线程是不是那个正在持有写锁的线程?// 如果当前线程不是持有写锁的那个线程,则返回false到队列中排队去;if (w == 0 || current != getExclusiveOwnerThread())return false;// 执行到此,说明同一个线程先是持有了写锁,然后还想继续占有写锁,那么就是重入的概念,随时欢迎重入锁if (w + exclusiveCount(acquires) > MAX_COUNT) // 当持有写锁的数量超过MAX_COUNT=65535时,则抛出异常,试想一下这个写锁如果达到了65535这个数量级的话,// 可能是递归导致的,可能是其他原因导致的,反正不管怎么着总不至于都溢出了吧;// 因此是有问题的,所以这里抛出了异常让调用方去查查到底是什么原因;throw new Error("Maximum lock count exceeded");// Reentrant acquiresetState(c + acquires); // 对于写锁的再次重入,来一个收一个,赋值写锁状态值,然后返回true继续执行临界区的代码return true;}// 执行到此,c=0,也就是说目前还没有线程占用读锁和写锁if (writerShouldBlock() || // 抽象方法需要子类来实现,根据写锁是否需要阻塞的标志来判断,true则需要阻塞,false则不需要阻塞// writerShouldBlock()在公平策略中,当有阻塞队列时则返回true需要阻塞,无阻塞队列时返回false不需要阻塞;// writerShouldBlock()在非公平策略中,永远都返回false写锁不需要阻塞;!compareAndSetState(c, c + acquires)) // 如果需要阻塞,则直接返回false到队列中排队去// 如果需要阻塞,则通过CAS尝试占用写锁资源,如果尝试占用写锁失败,说明由于并发c的值已经被改动了,所以还是乖乖到队列中排队去return false;setExclusiveOwnerThread(current); // 走到这里,说明写锁经过千辛万苦终于拿到写锁的执行权了,则可以继续执行临界区代码块了return true;}2、通过写锁writeLock.lock()最终调用的是Sync的tryAcquire尝试获取锁方式,从而可以得出几个结论:• 已持有读锁的线程不能再持有写锁;• 已持有写锁的线程可以再持有写锁,这和ReentrantLock的重入锁概念是一致的;• 已持有读锁的线程,其他线程是不能持有写锁的;• 已持有写锁的线程,其他线程是不能持有写锁的;3、至于返回false后面是如何进入阻塞队列的话,这里就不多讲了,因为前面已经讲过了,见( 原理剖析(第 005 篇)AQS工作原理分析 );

3.4、writerShouldBlock/readerShouldBlock

1、源码:/*** Fair version of Sync:公平策略版本的同步器;*/static final class FairSync extends Sync {private static final long serialVersionUID = -2274990926593161451L;/*** 公平策略的写锁是否需要阻塞,阻塞的判断依据就是:当有阻塞队列时则返回true需要阻塞,无阻塞队列时返回false不需要阻塞;*/final boolean writerShouldBlock() {return hasQueuedPredecessors();}/*** 公平策略的读锁是否需要阻塞,阻塞的判断依据就是:当有阻塞队列时则返回true需要阻塞,无阻塞队列时返回false不需要阻塞;*/final boolean readerShouldBlock() {return hasQueuedPredecessors();}}/*** Nonfair version of Sync:非公平策略版本的同步器;*/static final class NonfairSync extends Sync {private static final long serialVersionUID = -8159625535654395037L;/*** 非公平策略的写锁是否需要阻塞,阻塞的判断依据就是:直接是默认返回false,永远都返回false写锁不需要阻塞;*/final boolean writerShouldBlock() {return false; // writers can always barge}/*** 非公平策略的读锁是否需要阻塞,阻塞的判断依据就是:阻塞队列中的第一个结点是不是独占式结点,如果是则返回true表明读锁需要阻塞,否则返回false不需要阻塞;*/final boolean readerShouldBlock() {/* As a heuristic to avoid indefinite writer starvation,* block if the thread that momentarily appears to be head* of queue, if one exists, is a waiting writer.  This is* only a probabilistic effect since a new reader will not* block if there is a waiting writer behind other enabled* readers that have not yet drained from the queue.*/return apparentlyFirstQueuedIsExclusive();}}2、FairSync/NonfairSync主要重写了父类Sync的读锁、写锁是否需要阻塞,在公平策略与非公平策略中都有各自的实现;

3.5、tryRelease(int)

1、源码:// ReentrantReadWriteLock 的静态内部类 Sync 的 tryRelease 方法,写锁释放锁的核心方法protected final boolean tryRelease(int releases) {if (!isHeldExclusively()) // 如果当前线程没有获取独占式锁的话,也就是说当前线程没有持有写锁的话,那么就直接抛异常throw new IllegalMonitorStateException();// 之所以抛异常,是因为本来该方法就是持有写锁的线程来调用释放操作的,但是结果却发现当前线程自己没有吃有写锁,// 那岂不是尴尬,所以期间肯定出现了其他未知的问题,因此直接抛异常,告诉调用方,肯定有地方用错了还是啥啥啥的int nextc = getState() - releases; // 获取最新的锁资源值并且做减法操作,减去releasesboolean free = exclusiveCount(nextc) == 0; // 如果得出的nextc=0,那么说明持有写锁的线程已经完全被释放了if (free) // 一般情况下,通过锁资源值做减法操作,一般都会得到结果零,则设置独占式线程对象exclusiveOwnerThread为空setExclusiveOwnerThread(null); // setState(nextc); // 如果能执行到此,说明是重入锁,需要多重释放才能降低为零,反正如果没减至零最后都需要更新减后的结果值return free; // 返回true说明已经没有线程持有写锁了,返回false说明还有线程持有写锁}2、该方法主要讲解了写锁如何进行释放资源,最后不管做减法的结果如何,都会更新减法之后的结果赋值到state锁资源值;

3.6、tryAcquireShared(int)

1、源码:// ReentrantReadWriteLock 的静态内部类 Sync 的 tryAcquireShared 方法,读锁获取锁的核心方法protected final int tryAcquireShared(int unused) {/** Walkthrough:* 1. If write lock held by another thread, fail.* 2. Otherwise, this thread is eligible for*    lock wrt state, so ask if it should block*    because of queue policy. If not, try*    to grant by CASing state and updating count.*    Note that step does not check for reentrant*    acquires, which is postponed to full version*    to avoid having to check hold count in*    the more typical non-reentrant case.* 3. If step 2 fails either because thread*    apparently not eligible or CAS fails or count*    saturated, chain to version with full retry loop.*/Thread current = Thread.currentThread(); // 获取当前线程对象int c = getState(); // 获取内存中最新的锁资源值if (exclusiveCount(c) != 0 && // 如果有线程持有写锁getExclusiveOwnerThread() != current) // 并且持有写锁的线程不是当前线程return -1; // 那么则返回-1表明获取读锁失败,应该乖乖进入CLH阻塞队列int r = sharedCount(c); // 获取读锁共享计数// readerShouldBlock()在公平策略中,当有阻塞队列时则返回true需要阻塞,无阻塞队列时返回false不需要阻塞;// readerShouldBlock()在非公平策略中,阻塞队列中的第一个结点是不是独占式结点,如果是则返回true表明读锁需要阻塞,否则返回false不需要阻塞;if (!readerShouldBlock() && // 如果读锁不需要阻塞处理r < MAX_COUNT && // 如果读锁计数值没有超过最大限制值compareAndSetState(c, c + SHARED_UNIT)) { // 并且通过CAS尝试获取读锁资源// 能执行到if里面来,说明当前线程已经成功的突破了重重包围,准备看看如何接下来的处理;if (r == 0) { // 如果成功获取到读锁资源前,发现之前还没有任何线程持有读锁firstReader = current; // 则给firstReader对象赋值为第一个获取读锁的线程对象firstReaderHoldCount = 1; // 并且firstReaderHoldCount第一个线程持有读锁次数初始化次数为1} else if (firstReader == current) { // 能执行这个判断,说明读锁计数值肯定不为零,当首次获取读锁的线程正好是当前线程的话firstReaderHoldCount++; // 那么firstReaderHoldCount又加1,这里又可以认为重入锁的概念,但是这里重入的是读锁} else { // 如果执行到这里,说明当前持有读锁的线程不是当前线程HoldCounter rh = cachedHoldCounter; // 获取最近的一个成功获取读锁的线程的计数对象if (rh == null || rh.tid != getThreadId(current)) // 如果rh为空或者rh的线程id不是当前线程的话,cachedHoldCounter = rh = readHolds.get(); // 那么则将readHolds的计数对象取出来赋值给cachedHoldCounter// 意思就是说,readHolds里面有最近一次获取读锁的线程的一些简单的计数信息else if (rh.count == 0) // 当最近一个的那个计数对象count=0,则说明HoldCounter还刚刚被创立出来readHolds.set(rh); // 那么将rh这个对象直接赋值到readHolds中去rh.count++; // 并且次数累加一次}return 1; // 返回1说明当前已经成功的获取到了读锁,并且也成功的修改了state这么一个和锁资源密切相关的字段}// 执行到此,有3种情况:// 1、当读锁需要阻塞处理的话,则会执行到此;// 2、当读锁不需要阻塞处理,但是读锁的计数值超过了最大限制值MAX_COUNT=65535,那么也会执行到此;// 3、当读锁不需要阻塞处理,读锁计数值也没有超过最大限制值,但是通过CAS尝试占有读锁资源时失败了,也会执行到此// 总之一句话,没有顺利获取到读锁资源的线程,都会执行到这里来;return fullTryAcquireShared(current); // 获取读锁失败,则回炉重造通过自旋方式重试}2、通过readLock.lock()最终调用的是Sync的tryAcquireShared尝试获取锁方式,从而可以得出几个结论:• 已持有写锁的线程,其他线程是不能持有读锁的;• 已持有写锁的线程可以再持有读锁,这里我们称之为锁降级;• 已持有读锁的线程可以再持有读锁,这和ReentrantLock的重入锁概念是一致的;• 已持有读锁的线程,其他线程也可以持有读锁的;3、至于回炉重造的重试机制和tryAcquireShared操作方式以及代码非常类似,这里就不再详讲了;

3.7、tryReleaseShared(int)

1、源码:// ReentrantReadWriteLock 的静态内部类 Sync 的 tryReleaseShared 方法,读锁释放锁的核心方法protected final boolean tryReleaseShared(int unused) {Thread current = Thread.currentThread(); // 获取当前线程对象if (firstReader == current) { // 如果首次获取读锁的线程为当前线程的话// assert firstReaderHoldCount > 0;if (firstReaderHoldCount == 1) // 如果此刻firstReaderHoldCount次数正好为1的话,说明该线程的读锁没有重入firstReader = null; // 则直接将首次获取读锁的线程置为空即可elsefirstReaderHoldCount--; // 若firstReaderHoldCount不为1,则肯定是读锁重入了,则需要自减1操作;} else {// 执行到此,说明当前要释放读锁的线程不是那个首次获取到读锁的线程HoldCounter rh = cachedHoldCounter;if (rh == null || rh.tid != getThreadId(current)) // 获取最近的那个线程对象,如果不是当前线程的话rh = readHolds.get(); // 那么则通过readHolds获取最近的那个计数对象int count = rh.count; // 取出count值if (count <= 1) { // 若小于等于1,那么自减1就没了,所以减都没减了,直接移除掉,简单干脆readHolds.remove(); // 直接移除if (count <= 0) // throw unmatchedUnlockException();}--rh.count; // 如果大于1的话,则还有的减,那么就自减1操作}for (;;) { // 自旋的死循环操作方式int c = getState(); // 获取最新的锁资源值int nextc = c - SHARED_UNIT; // 通过计算高位减1处理if (compareAndSetState(c, nextc)) // 通过尝试CAS正好设置成功的话,那么则返回nextc与0的比较// Releasing the read lock has no effect on readers,// but it may allow waiting writers to proceed if// both read and write locks are now free.return nextc == 0; // 不管如何,只要CAS成功,则表明读锁次数值已经被降低释放了一次了// 执行到此,说明由于并发的原因导致CAS失败,所以需要继续循环再次操作释放读锁次数操作}}2、该方法主要讲解了读锁如何进行释放资源,如果首次释放失败的话,则会通过自旋的方式继续尝试释放资源,直到成功为止;

四、总结

1、这里有许多其它更底层的没有分析,因为都是AQS内部的基类方法了,而这些基类方法都在之前介绍过了,如果大家不记得的就去翻前面的篇章( 原理剖析(第 005 篇)AQS工作原理分析 );2、经过上面的一系列分析之后,在这里我再来总结一下ReentrantReadWriteLock的流程的一些特性;// ReentrantReadWriteLock.WriteLock.lock()特性:• 已持有读锁的线程不能再持有写锁;• 已持有写锁的线程可以再持有写锁,这和ReentrantLock的重入锁概念是一致的;• 已持有读锁的线程,其他线程是不能持有写锁的;• 已持有写锁的线程,其他线程是不能持有写锁的;// ReentrantReadWriteLock.ReadLock.lock()特性:• 已持有写锁的线程,其他线程是不能持有读锁的;• 已持有写锁的线程可以再持有读锁,这里我们称之为锁降级;• 已持有读锁的线程可以再持有读锁,这和ReentrantLock的重入锁概念是一致的;• 已持有读锁的线程,其他线程也可以持有读锁的;3、然而将上面的WriteLock\ReadLock特性进行合并为:• 已持有写锁的线程可以再持有写锁,这和ReentrantLock的重入锁概念是一致的;• 已持有写锁的线程,其他线程是不能持有读锁、写锁的;• 已持有写锁的线程可以再持有读锁,这里我们称之为锁降级;• 已持有读锁的线程可以再持有读锁,这和ReentrantLock的重入锁概念是一致的;• 已持有读锁的线程,其他线程也可以持有读锁的;  • 已持有读锁的线程,其他线程(同时也包括已持有读锁的线程)是不能持有写锁的;4、排除可重入的特性,再精炼合并特性为:• 写锁会排斥读锁、写锁,但是读锁会阻塞写锁;• 写锁可以降级为读锁,但读锁不能升级为写锁;

五、下载地址

https://gitee.com/ylimhhmily/SpringCloudTutorial.git

SpringCloudTutorial交流QQ群: 235322432

SpringCloudTutorial交流微信群: 微信沟通群二维码图片链接

欢迎关注,您的肯定是对我最大的支持!!!

转载于:https://my.oschina.net/hmilyylimh/blog/1634883

原理剖析(第 009 篇)ReentrantReadWriteLock工作原理分析相关推荐

  1. 【es】es 分布式一致性原理剖析(二)-Meta篇

    1.概述 转载:Elasticsearch分布式一致性原理剖析(二)-Meta篇 前言 "Elasticsearch分布式一致性原理剖析"系列将会对Elasticsearch的分布 ...

  2. 【es】es 分布式一致性原理剖析(三)-Data篇

    1.概述 转载:Elasticsearch分布式一致性原理剖析(三)-Data篇 前言 "Elasticsearch分布式一致性原理剖析"系列将会对Elasticsearch的分布 ...

  3. 【原理】#01红外热成像仪的工作原理介绍

    红外热成像仪是电测中比较特殊的一种仪器设备,在温度测试领域有着广泛的应用. 红外热成像仪测温的原理是怎么样的?如何选择合适的热成像仪? 安泰小课堂将分两期视频为大家进行揭秘. 本期内容将重点讲解红外热 ...

  4. Elasticsearch分布式一致性原理剖析(一)-节点篇

    2019独角兽企业重金招聘Python工程师标准>>> 摘要: ES目前是最流行的开源分布式搜索引擎系统,其使用Lucene作为单机存储引擎并提供强大的搜索查询能力.学习其搜索原理, ...

  5. Elasticsearch分布式一致性原理剖析(三)-Data篇

    前言 "Elasticsearch分布式一致性原理剖析"系列将会对Elasticsearch的分布式一致性原理进行详细的剖析,介绍其实现方式.原理以及其存在的问题等(基于6.2版本 ...

  6. 125KHz 100cm ID 读卡电路_深入剖析锂电池保护电路的工作原理

    很早之前写过一篇<锂电子电池简介>,今天介绍一下常见的锂电池保护电路的工作原理. ▉ 前言 举一个不恰当的例子,电池的充放电就像孩子喝母乳一样. 1,如果一直让孩子喝,家长不加以控制,那么 ...

  7. Spark原理篇之工作原理

    1 Spark背景 Spark是一个加州大学伯克利分校(UC Berkeley AMP)开发的一个分布式数据快速分析项目.它的核心技术是弹性分布式数据集(Resilient distributed d ...

  8. javascript原理_JavaScript程序包管理器工作原理简介

    javascript原理 by Shubheksha 通过Shubheksha JavaScript程序包管理器工作原理简介 (An introduction to how JavaScript pa ...

  9. BP神经网络原理及其应用,bp神经网络的工作原理

    1.BP神经网络的工作原理 人工神经网络就是模拟人思维的第二种方式.这是一个非线性动力学系统,其特色在于信息的分布式存储和并行协同处理.虽然单个神经元的结构极其简单,功能有限,但大量神经元构成的网络系 ...

最新文章

  1. 我在家乡写代码(一)
  2. Sql Server 2008 精简版(Express)和管理工具的安装以及必须重新启动计算机才能安装 SQLServer的问题和第一次使用sqlexpress的连接问题
  3. $_SERVER['SCRIPT_NAME']
  4. 【转】两种方法教你在Ubuntu下轻松关闭触摸板(TinkPad)
  5. Spring Boot国际化支持
  6. why my SAP CRM One Order custom callback is not called
  7. uniapp /deep/设置uni-app组件样式时 h5生效 小程序失效问题解决
  8. android软键盘上推ui解决
  9. 三星Galaxy Fold 2渲染图曝光:怎么折是个问题
  10. 读 利用python进行数据分析 后感
  11. Web前端程序员必备 前端面试题汇总(1)
  12. linux更新字体库失败,wps for linux 字体库缺失问题的解决办法
  13. 2021年中国键合对准系统市场趋势报告、技术动态创新及2027年市场预测
  14. Android开发虚拟机测试没问题,真机调试就出现问题,总是闪退!10秒解决!!
  15. 大学四年,我把私藏「B站」 20 个学习 UP 主贡献出来!
  16. 学会自我欣赏,将缺点变为有点
  17. C switch 语句
  18. 20201022-成信大-C语言程序设计-20201学期《C语言程序设计B》C-trainingExercises02
  19. Alibaba SWE 实习岗 笔试题 JAVA
  20. 机器人简化图画手绘图_如何画机器人的简笔画 经验告诉你该这样

热门文章

  1. 网页闯关游戏(riddle webgame)--仿微信聊天的前端页面设计和难点
  2. Openssl verify命令
  3. 1.窗体与界面设计-菜单应用实例
  4. RBAC用户角色权限设计方案(转)
  5. IE8的css hack
  6. linux之lsof使用技巧
  7. 2021年广东高考各科成绩查询,2017年广东高考成绩五种查询方式一览
  8. Django实现微信消息推送
  9. gitlab+jenkins+ansible集成持续发布
  10. 《Cisco防火墙》一8.7 通过NAT规则定义连接限制