纠纷奇闻社交美文家庭
投稿投诉
家庭城市
爱好生活
创业男女
能力餐饮
美文职业
心理周易
母婴奇趣
两性技能
社交传统
新闻范文
工作个人
思考社会
作文职场
家居中考
兴趣安全
解密魅力
奇闻笑话
写作笔记
阅读企业
饮食时事
纠纷案例
初中历史
说说童话
乐趣治疗

深入AQS原理我画了35张图就是为了让你深入AQS

12月16日 老巫婆投稿
  前言
  谈到并发,我们不得不说AQS(AbstractQueuedSynchronizer),所谓的AQS即是抽象的队列式的同步器,内部定义了很多锁相关的方法,我们熟知的ReentrantLock、ReentrantReadWriteLock、CountDownLatch、Semaphore等都是基于AQS来实现的。
  我们先看下AQS相关的UML图:
  思维导图:
  AQS实现原理
  AQS中维护了一个volatileintstate(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。
  这里volatile能够保证多线程下的可见性,当state1则代表当前对象锁已经被占有,其他线程来加锁时则会失败,加锁失败的线程会被放入一个FIFO的等待队列中,比列会被UNSAFE。park()操作挂起,等待其他获取锁的线程释放锁才能够被唤醒。
  另外state的操作都是通过CAS来保证其并发修改的安全性。
  具体原理我们可以用一张图来简单概括:
  AQS中提供了很多关于锁的实现方法,getState():获取锁的标志state值setState():设置锁的标志state值tryAcquire(int):独占方式获取锁。尝试获取资源,成功则返回true,失败则返回false。tryRelease(int):独占方式释放锁。尝试释放资源,成功则返回true,失败则返回false。
  这里还有一些方法并没有列出来,接下来我们以ReentrantLock作为突破点通过源码和画图的形式一步步了解AQS内部实现原理。目录结构
  文章准备模拟多线程竞争锁、释放锁的场景来进行分析AQS源码:
  三个线程(线程一、线程二、线程三)同时来加锁释放锁
  目录如下:线程一加锁成功时AQS内部实现线程二三加锁失败时AQS中等待队列的数据模型线程一释放锁及线程二获取锁实现原理通过线程场景来讲解公平锁具体实现原理通过线程场景来讲解Condition中await()和signal()实现原理
  这里会通过画图来分析每个线程加锁、释放锁后AQS内部的数据结构和实现原理场景分析线程一加锁成功
  如果同时有三个线程并发抢占锁,此时线程一抢占锁成功,线程二和线程三抢占锁失败,具体执行流程如下:
  此时AQS内部数据为:
  线程二、线程三加锁失败:
  有图可以看出,等待队列中的节点Node是一个双向链表,这里SIGNAL是Node中waitStatus属性,Node中还有一个nextWaiter属性,这个并未在图中画出来,这个到后面Condition会具体讲解的。
  具体看下抢占锁代码实现:
  java。util。concurrent。locks。ReentrantLock。NonfairSync:staticfinalclassNonfairSyncextendsSync{finalvoidlock(){if(compareAndSetState(0,1))setExclusiveOwnerThread(Thread。currentThread());elseacquire(1);}protectedfinalbooleantryAcquire(intacquires){returnnonfairTryAcquire(acquires);}}
  这里使用的ReentrantLock非公平锁,线程进来直接利用CAS尝试抢占锁,如果抢占成功state值回被改为1,且设置对象独占锁线程为当前线程。如下所示:protectedfinalbooleancompareAndSetState(intexpect,intupdate){returnunsafe。compareAndSwapInt(this,stateOffset,expect,update);}protectedfinalvoidsetExclusiveOwnerThread(Threadthread){exclusiveOwnerT}线程二抢占锁失败
  我们按照真实场景来分析,线程一抢占锁成功后,state变为1,线程二通过CAS修改state变量必然会失败。此时AQS中FIFO(FirstInFirstOut先进先出)队列中数据如图所示:
  我们将线程二执行的逻辑一步步拆解来看:
  java。util。concurrent。locks。AbstractQueuedSynchronizer。acquire():publicfinalvoidacquire(intarg){if(!tryAcquire(arg)acquireQueued(addWaiter(Node。EXCLUSIVE),arg))selfInterrupt();}
  先看看tryAcquire()的具体实现:java。util。concurrent。locks。ReentrantLock。nonfairTryAcquire():finalbooleannonfairTryAcquire(intacquires){finalThreadcurrentThread。currentThread();intcgetState();if(c0){if(compareAndSetState(0,acquires)){setExclusiveOwnerThread(current);}}elseif(currentgetExclusiveOwnerThread()){if(nextc0)thrownewError(Maximumlockcountexceeded);setState(nextc);}}
  nonfairTryAcquire()方法中首先会获取state的值,如果不为0则说明当前对象的锁已经被其他线程所占有,接着判断占有锁的线程是否为当前线程,如果是则累加state值,这就是可重入锁的具体实现,累加state值,释放锁的时候也要依次递减state值。
  如果state为0,则执行CAS操作,尝试更新state值为1,如果更新成功则代表当前线程加锁成功。
  以线程二为例,因为线程一已经将state修改为1,所以线程二通过CAS修改state的值不会成功。加锁失败。
  线程二执行tryAcquire()后会返回false,接着执行addWaiter(Node。EXCLUSIVE)逻辑,将自己加入到一个FIFO等待队列中,代码实现如下:
  java。util。concurrent。locks。AbstractQueuedSynchronizer。addWaiter():privateNodeaddWaiter(Nodemode){NodenodenewNode(Thread。currentThread(),mode);Nif(pred!null){node。if(compareAndSetTail(pred,node)){pred。}}enq(node);}
  这段代码首先会创建一个和当前线程绑定的Node节点,Node为双向链表。此时等待对内中的tail指针为空,直接调用enq(node)方法将当前线程加入等待队列尾部:privateNodeenq(finalNodenode){for(;;){Nif(tnull){if(compareAndSetHead(newNode()))}else{node。if(compareAndSetTail(t,node)){t。}}}}
  第一遍循环时tail指针为空,进入if逻辑,使用CAS操作设置head指针,将head指向一个新创建的Node节点。此时AQS中数据:
  执行完成之后,head、tail、t都指向第一个Node元素。
  接着执行第二遍循环,进入else逻辑,此时已经有了head节点,这里要操作的就是将线程二对应的Node节点挂到head节点后面。此时队列中就有了两个Node节点:
  addWaiter()方法执行完后,会返回当前线程创建的节点信息。继续往后执行acquireQueued(addWaiter(Node。EXCLUSIVE),arg)逻辑,此时传入的参数为线程二对应的Node节点信息:
  java。util。concurrent。locks。AbstractQueuedSynchronizer。acquireQueued():finalbooleanacquireQueued(finalNodenode,intarg){try{for(;;){finalNodepnode。predecessor();if(pheadtryAcquire(arg)){setHead(node);p。}if(shouldParkAfterFailedAcquire(p,node)parkAndChecknIterrupt())}}finally{if(failed)cancelAcquire(node);}}privatestaticbooleanshouldParkAfterFailedAcquire(Nodepred,Nodenode){intwspred。waitSif(wsNode。SIGNAL)if(ws0){do{node。prevpredpred。}while(pred。waitStatus0);pred。}else{compareAndSetWaitStatus(pred,ws,Node。SIGNAL);}}privatefinalbooleanparkAndCheckInterrupt(){LockSupport。park(this);returnThread。interrupted();}
  acquireQueued()这个方法会先判断当前传入的Node对应的前置节点是否为head,如果是则尝试加锁。加锁成功过则将当前节点设置为head节点,然后空置之前的head节点,方便后续被垃圾回收掉。
  如果加锁失败或者Node的前置节点不是head节点,就会通过shouldParkAfterFailedAcquire方法将head节点的waitStatus变为了SIGNAL1,最后执行parkAndChecknIterrupt方法,调用LockSupport。park()挂起当前线程。
  此时AQS中的数据如下图:
  此时线程二就静静的待在AQS的等待队列里面了,等着其他线程释放锁来唤醒它。线程三抢占锁失败
  看完了线程二抢占锁失败的分析,那么再来分析线程三抢占锁失败就很简单了,先看看addWaiter(Nodemode)方法:privateNodeaddWaiter(Nodemode){NodenodenewNode(Thread。currentThread(),mode);Nif(pred!null){node。if(compareAndSetTail(pred,node)){pred。}}enq(node);}
  此时等待队列的tail节点指向线程二,进入if逻辑后,通过CAS指令将tail节点重新指向线程三。接着线程三调用enq()方法执行入队操作,和上面线程二执行方式是一致的,入队后会修改线程二对应的Node中的waitStatusSIGNAL。最后线程三也会被挂起。此时等待队列的数据如图:
  线程一释放锁
  现在来分析下释放锁的过程,首先是线程一释放锁,释放锁后会唤醒head节点的后置节点,也就是我们现在的线程二,具体操作流程如下:
  执行完后等待队列数据如下:
  此时线程二已经被唤醒,继续尝试获取锁,如果获取锁失败,则会继续被挂起。如果获取锁成功,则AQS中数据如图:
  接着还是一步步拆解来看,先看看线程一释放锁的代码:
  java。util。concurrent。locks。AbstractQueuedSynchronizer。release()publicfinalbooleanrelease(intarg){if(tryRelease(arg)){Nif(h!nullh。waitStatus!0)unparkSuccessor(h);}}
  这里首先会执行tryRelease()方法,这个方法具体实现在ReentrantLock中,如果tryRelease执行成功,则继续判断head节点的waitStatus是否为0,前面我们已经看到过,head的waitStatue为SIGNAL(1),这里就会执行unparkSuccessor()方法来唤醒head的后置节点,也就是我们上面图中线程二对应的Node节点。
  此时看ReentrantLock。tryRelease()中的具体实现:protectedfinalbooleantryRelease(intreleases){intcgetState()if(Thread。currentThread()!getExclusiveOwnerThread())thrownewIllegalMonitorStateException();if(c0){setExclusiveOwnerThread(null);}setState(c);}
  执行完ReentrantLock。tryRelease()后,state被设置成0,Lock对象的独占锁被设置为null。此时看下AQS中的数据:
  接着执行java。util。concurrent。locks。AbstractQueuedSynchronizer。unparkSuccessor()方法,唤醒head的后置节点:privatevoidunparkSuccessor(Nodenode){intwsnode。waitSif(ws0)compareAndSetWaitStatus(node,ws,0);Nodesnode。if(snulls。waitStatus0){for(Nt!nullt!tt。prev)if(t。waitStatus0)}if(s!null)LockSupport。unpark(s。thread);}
  这里主要是将head节点的waitStatus设置为0,然后解除head节点next的指向,使head节点空置,等待着被垃圾回收。
  此时重新将head指针指向线程二对应的Node节点,且使用LockSupport。unpark方法来唤醒线程二。
  被唤醒的线程二会接着尝试获取锁,用CAS指令修改state数据。执行完成后可以查看AQS中数据:
  此时线程二被唤醒,线程二接着之前被park的地方继续执行,继续执行acquireQueued()方法。线程二唤醒继续加锁finalbooleanacquireQueued(finalNodenode,intarg){try{for(;;){finalNodepnode。predecessor();if(pheadtryAcquire(arg)){setHead(node);p。}if(shouldParkAfterFailedAcquire(p,node)parkAndCheckInterrupt())}}finally{if(failed)cancelAcquire(node);}}
  此时线程二被唤醒,继续执行for循环,判断线程二的前置节点是否为head,如果是则继续使用tryAcquire()方法来尝试获取锁,其实就是使用CAS操作来修改state值,如果修改成功则代表获取锁成功。接着将线程二设置为head节点,然后空置之前的head节点数据,被空置的节点数据等着被垃圾回收。
  此时线程三获取锁成功,AQS中队列数据如下:
  等待队列中的数据都等待着被垃圾回收。线程二释放锁线程三加锁
  当线程二释放锁时,会唤醒被挂起的线程三,流程和上面大致相同,被唤醒的线程三会再次尝试加锁,具体代码可以参考上面内容。具体流程图如下:
  此时AQS中队列数据如图:
  公平锁实现原理
  上面所有的加锁场景都是基于非公平锁来实现的,非公平锁是ReentrantLock的默认实现,那我们接着来看一下公平锁的实现原理,这里先用一张图来解释公平锁和非公平锁的区别:
  非公平锁执行流程:
  这里我们还是用之前的线程模型来举例子,当线程二释放锁的时候,唤醒被挂起的线程三,线程三执行tryAcquire()方法使用CAS操作来尝试修改state值,如果此时又来了一个线程四也来执行加锁操作,同样会执行tryAcquire()方法。
  这种情况就会出现竞争,线程四如果获取锁成功,线程三仍然需要待在等待队列中被挂起。这就是所谓的非公平锁,线程三辛辛苦苦排队等到自己获取锁,却眼巴巴的看到线程四插队获取到了锁。
  公平锁执行流程:
  公平锁在加锁的时候,会先判断AQS等待队列中是存在节点,如果存在节点则会直接入队等待,具体代码如下。
  公平锁在获取锁是也是首先会执行acquire()方法,只不过公平锁单独实现了tryAcquire()方法:
  java。util。concurrent。locks。AbstractQueuedSynchronizer。acquire():publicfinalvoidacquire(intarg){if(!tryAcquire(arg)acquireQueued(addWaiter(Node。EXCLUSIVE),arg))selfInterrupt();}
  这里会执行ReentrantLock中公平锁的tryAcquire()方法
  java。util。concurrent。locks。ReentrantLock。FairSync。tryAcquire():staticfinalclassFairSyncextendsSync{protectedfinalbooleantryAcquire(intacquires){finalThreadcurrentThread。currentThread();intcgetState();if(c0){if(!hasQueuedPredecessors()compareAndSetState(0,acquires)){setExclusiveOwnerThread(current);}}elseif(currentgetExclusiveOwnerThread()){if(nextc0)thrownewError(Maximumlockcountexceeded);setState(nextc);}}}
  这里会先判断state值,如果不为0且获取锁的线程不是当前线程,直接返回false代表获取锁失败,被加入等待队列。如果是当前线程则可重入获取锁。
  如果state0则代表此时没有线程持有锁,执行hasQueuedPredecessors()判断AQS等待队列中是否有元素存在,如果存在其他等待线程,那么自己也会加入到等待队列尾部,做到真正的先来后到,有序加锁。具体代码如下:
  java。util。concurrent。locks。AbstractQueuedSynchronizer。hasQueuedPredecessors():publicfinalbooleanhasQueuedPredecessors(){NNNreturnh!t((sh。next)nulls。thread!Thread。currentThread());}
  这段代码很有意思,返回false代表队列中没有节点或者仅有一个节点是当前线程创建的节点。返回true则代表队列中存在等待节点,当前线程需要入队等待。
  先判断head是否等于tail,如果队列中只有一个Node节点,那么head会等于tail,接着判断head的后置节点,这里肯定会是null,如果此Node节点对应的线程和当前的线程是同一个线程,那么则会返回false,代表没有等待节点或者等待节点就是当前线程创建的Node节点。此时当前线程会尝试获取锁。
  如果head和tail不相等,说明队列中有等待线程创建的节点,此时直接返回true,如果只有一个节点,而此节点的线程和当前线程不一致,也会返回true
  非公平锁和公平锁的区别:非公平锁性能高于公平锁性能。非公平锁可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量
  非公平锁性能虽然优于公平锁,但是会存在导致线程饥饿的情况。在最坏的情况下,可能存在某个线程一直获取不到锁。不过相比性能而言,饥饿问题可以暂时忽略,这可能就是ReentrantLock默认创建非公平锁的原因之一了。Condition实现原理Condition简介
  上面已经介绍了AQS所提供的核心功能,当然它还有很多其他的特性,这里我们来继续说下Condition这个组件。
  Condition是在java1。5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition中的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition
  其中AbstractQueueSynchronizer中实现了Condition中的方法,主要对外提供awaite(Object。wait())和signal(Object。notify())调用。ConditionDemo示例
  使用示例代码:ReentrantLock实现源码学习author一枝花算不算浪漫date20204287:20publicclassReentrantLockDemo{staticReentrantLocklocknewReentrantLock();publicstaticvoidmain(String〔〕args){Conditionconditionlock。newCondition();newThread((){lock。lock();try{System。out。println(线程一加锁成功);System。out。println(线程一执行await被挂起);condition。await();System。out。println(线程一被唤醒成功);}catch(Exceptione){e。printStackTrace();}finally{lock。unlock();System。out。println(线程一释放锁成功);}})。start();newThread((){lock。lock();try{System。out。println(线程二加锁成功);condition。signal();System。out。println(线程二唤醒线程一);}finally{lock。unlock();System。out。println(线程二释放锁成功);}})。start();}}
  执行结果如下图:
  这里线程一先获取锁,然后使用await()方法挂起当前线程并释放锁,线程二获取锁后使用signal唤醒线程一。Condition实现原理图解
  我们还是用上面的demo作为实例,执行的流程如下:
  线程一执行await()方法:
  先看下具体的代码实现,java。util。concurrent。locks。AbstractQueuedSynchronizer。ConditionObject。await():publicfinalvoidawait()throwsInterruptedException{if(Thread。interrupted())thrownewInterruptedException();NodenodeaddConditionWaiter();intsavedStatefullyRelease(node);intinterruptMode0;while(!isOnSyncQueue(node)){LockSupport。park(this);if((interruptModecheckInterruptWhileWaiting(node))!0)}if(acquireQueued(node,savedState)interruptMode!THROWIE)interruptModeREINTERRUPT;if(node。nextWaiter!null)unlinkCancelledWaiters();if(interruptMode!0)reportInterruptAfterWait(interruptMode);}
  await()方法中首先调用addConditionWaiter()将当前线程加入到Condition队列中。
  执行完后我们可以看下Condition队列中的数据:
  具体实现代码为:privateNodeaddConditionWaiter(){NodetlastWif(t!nullt。waitStatus!Node。CONDITION){unlinkCancelledWaiters();tlastW}NodenodenewNode(Thread。currentThread(),Node。CONDITION);if(tnull)firstWelset。nextWlastW}
  这里会用当前线程创建一个Node节点,waitStatus为CONDITION。接着会释放该节点的锁,调用之前解析过的release()方法,释放锁后此时会唤醒被挂起的线程二,线程二会继续尝试获取锁。
  接着调用isOnSyncQueue()方法判断当前节点是否为Condition队列中的头部节点,如果是则调用LockSupport。park(this)挂起Condition中当前线程。此时线程一被挂起,线程二获取锁成功。
  具体流程如下图:
  线程二执行signal()方法:
  首先我们考虑下线程二已经获取到锁,此时AQS等待队列中已经没有了数据。
  接着就来看看线程二唤醒线程一的具体执行流程:publicfinalvoidsignal(){if(!isHeldExclusively())thrownewIllegalMonitorStateException();NodefirstfirstWif(first!null)doSignal(first);}
  先判断当前线程是否为获取锁的线程,如果不是则直接抛出异常。接着调用doSignal()方法来唤醒线程。privatevoiddoSignal(Nodefirst){do{if((firstWaiterfirst。nextWaiter)null)lastWfirst。nextW}while(!transferForSignal(first)(firstfirstWaiter)!null);}finalbooleantransferForSignal(Nodenode){if(!compareAndSetWaitStatus(node,Node。CONDITION,0))Nodepenq(node);intwsp。waitSif(ws0!compareAndSetWaitStatus(p,ws,Node。SIGNAL))LockSupport。unpark(node。thread);}Insertsnodeintoqueue,initializingifnecessary。Seepictureabove。paramnodethenodetoinsertreturnnodespredecessorprivateNodeenq(finalNodenode){for(;;){Nif(tnull){if(compareAndSetHead(newNode()))}else{node。if(compareAndSetTail(t,node)){t。}}}}
  这里先从transferForSignal()方法来看,通过上面的分析我们知道Condition队列中只有线程一创建的一个Node节点,且waitStatue为CONDITION,先通过CAS修改当前节点waitStatus为0,然后执行enq()方法将当前线程加入到等待队列中,并返回当前线程的前置节点。
  加入等待队列的代码在上面也已经分析过,此时等待队列中数据如下图:
  接着开始通过CAS修改当前节点的前置节点waitStatus为SIGNAL,并且唤醒当前线程。此时AQS中等待队列数据为:
  线程一被唤醒后,继续执行await()方法中的while循环。publicfinalvoidawait()throwsInterruptedException{if(Thread。interrupted())thrownewInterruptedException();NodenodeaddConditionWaiter();intsavedStatefullyRelease(node);intinterruptMode0;while(!isOnSyncQueue(node)){LockSupport。park(this);if((interruptModecheckInterruptWhileWaiting(node))!0)}if(acquireQueued(node,savedState)interruptMode!THROWIE)interruptModeREINTERRUPT;if(node。nextWaiter!null)unlinkCancelledWaiters();if(interruptMode!0)reportInterruptAfterWait(interruptMode);}
  因为此时线程一的waitStatus已经被修改为0,所以执行isOnSyncQueue()方法会返回false。跳出while循环。
  接着执行acquireQueued()方法,这里之前也有讲过,尝试重新获取锁,如果获取锁失败继续会被挂起。直到另外线程释放锁才被唤醒。finalbooleanacquireQueued(finalNodenode,intarg){try{for(;;){finalNodepnode。predecessor();if(pheadtryAcquire(arg)){setHead(node);p。}if(shouldParkAfterFailedAcquire(p,node)parkAndCheckInterrupt())}}finally{if(failed)cancelAcquire(node);}}
  此时线程一的流程都已经分析完了,等线程二释放锁后,线程一会继续重试获取锁,流程到此终结。Condition总结
  我们总结下Condition和waitnotify的比较:Condition可以精准的对多个不同条件进行控制,waitnotify只能和synchronized关键字一起使用,并且只能唤醒一个或者全部的等待队列;Condition需要使用Lock进行控制,使用的时候要注意lock()后及时的unlock(),Condition有类似于await的机制,因此不会产生加锁方式而产生的死锁出现,同时底层实现的是parkunpark的机制,因此也不会产生先唤醒再挂起的死锁,一句话就是不会产生死锁,但是waitnotify会产生先唤醒再挂起的死锁。总结
  这里用了一步一图的方式结合三个线程依次加锁释放锁来展示了ReentrantLock的实现方式和实现原理,而ReentrantLock底层就是基于AQS实现的,所以我们也对AQS有了深刻的理解。
  另外还介绍了公平锁与非公平锁的实现原理,Condition的实现原理,基本上都是使用源码绘图的讲解方式,尽量让大家更容易去理解。
  一枝花算不算浪漫
  https:www。cnblogs。comwangmengp12816829。html
投诉 评论 转载

消息称任天堂将为Switch推出新手柄IT之家9月17日消息据报道,FCC美国联邦通信委员会最近透露的信息显示,任天堂可能将推出适用于Switch的新款游戏手柄。据FCC公开的信息显示,任天堂的产品描述包含任……模玩资讯phat无职转生洛琪希米格路迪亚17立体人形日本玩具厂商Phat!(株式会社)所开发以及发售的高质量美少女模型系列,向来是许多粉丝的收藏最爱,该系列将推出著名小说与动画作品《无职转生》中的重要女主角之一洛琪希米格路迪亚(……韩流又至,中国围棋该清醒了10月28日,第二十六届三星杯四强战罢,中国两人杨鼎新和赵晨宇分别负于韩国的申真谞和朴廷桓。韩国包揽了此次三星杯的冠亚军。中国围棋的世界大赛有一段长达近20年黑暗的时光,……任天堂将把GameBoy掌机游戏带到SwitchOnlineIT之家9月4日消息根据外媒TheVerge消息,近日任天堂在播客中证实,计划把经典的GameBoy系列掌机中的游戏带到SwitchOnline。该订阅服务为Switch主机用……战地2042PC配置要求公布最低GTX1050TiIT之家8月4日消息EA旗下DICE工作室官宣《战地2042》将于10月23日发售,支持中文配音。根据邀请测试邮件,该游戏将于8月12日至8月15日分6次进行技术测试,并……战地2042Beta公测将于9月开启,预购玩家可提前参加IT之家7月24日消息EA旗下DICE工作室上个月官宣了《战地2042》,游戏将于10月23日发售,支持中文配音。官方近日宣布,《战地2042》将于9月开启Beta公测,……2021买手机追求个性?这几款都是锐利异类,大街上鲜有人用虽然大部分用户会考虑购买主流手机,但仍然有一部分用户,喜欢那些用的人不多,品牌小众,销量也低的机型,有的人是为了充值信仰、有的人是品牌忠实粉,而有的人他就是喜欢锐利异类,所以如……深入AQS原理我画了35张图就是为了让你深入AQS前言谈到并发,我们不得不说AQS(AbstractQueuedSynchronizer),所谓的AQS即是抽象的队列式的同步器,内部定义了很多锁相关的方法,我们熟知的Re……周日寻幽探胜大鹏所城周日休闲时光,喜欢寻幽仿古穿越时光遂道,与古远的文化对话,期间总有一些细节意境氛围深深震撼着我。早前从沙湾古镇到赤坎古镇,从龙湖古寨到大鹏所城,每处古迹都以其历史与人文的厚重感……Meta研究根据HDR图像重建真实世界反射属性和阴影的方法查看引用信息源请点击:映维网根据HDR图像重建真实世界反射属性和阴影(映维网2021年11月02日)忠实地恢复世界的副本对于虚拟现实、增强现实和混合现实而言至关重要……使命召唤手游上线竞技爆破模式提升游戏竞技和策略性IT之家3月15日消息根据使命召唤手游官方的消息,竞技爆破模式已于今日正式登场。竞技爆破在现有的经典爆破的基础上,增加了更多的限制,大大提升了竞技和策略性。IT之家了解到……PS1风格恐怖游戏ChasingStatic上架Steam平IT之家3月14日消息近日,游戏发行商RatalaikaGames和开发商HeadwareGames宣布,将于2021年第3季度发布PS1风格恐怖游戏《ChasingStati……
科普下北回归线的纬度是多少是什么意思甘肃天水装修300万纯利润100万饭店倒闭,只因老板势利眼少女时代成员关系(少女时代内部关系分析)苹果手机怎么查微信加过的好友的聊天记录吗(怎么找到微信的其他这把椅子,让一代泰斗王世襄苦寻40年!怎么系鞋带(8种漂亮的系鞋带方法)数学知识等腰三角形三线合一逆定理客家祖地(带你走进著名的客家祖地之一清流县)街头篮球交易吧(街头篮球4代超特创建碎片)芭芭多芦荟(芭芭多芦荟产品)四连冠!山东全运会58金,再度称雄,广东第二,山东体育强在哪6个小镇被淘汰当地政府收回1000万启动资金

友情链接:中准网聚热点快百科快传网快生活快软网快好知文好找美丽时装彩妆资讯历史明星乐活安卓数码常识驾车健康苹果问答网络发型电视车载室内电影游戏科学音乐整形