热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

基于ReentrantLock的AQS源码解析(一)

众所周知:ReentrantLock可以是公平的也可以是非公平的。所谓公平:就是先来的肯定比后来的先获取到锁。也就是不能插队。所谓非公平:

众所周知:ReentrantLock可以是公平的也可以是非公平的。
所谓公平:就是先来的肯定比后来的先获取到锁。也就是不能插队。
所谓非公平:就是说,后来的可以比先来的先获取到锁,也就是说可以插队。

ReentrantLock reentrantLock = new ReentrantLock(true);//公平ReentrantLock reentrantLock = new ReentrantLock();//非公平

以公平为例子进行讲解。

ReentrantLock reentrantLock = new ReentrantLock(true);try{reentrantLock.lock();//业务逻辑}catch (Exception e){e.printStackTrace();}finally {reentrantLock.unlock();}

我们知道,ReentrantLock 使用起来就向上面那样。一个lock一个unlock即可。
首先看lock方法:

public void lock() {sync.lock();}

继续跟进去,最后可以看到,调用的是acquire(1):

final void lock() {acquire(1);}

acquire方法:

public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}

重点从这里才开始,上面的都是依次跟就行。
首先进行了一个if判断。

tryAcquire

第一个方法:tryAcquire

protected final boolean tryAcquire(int acquires) {//这里是公平锁的tryAcquire方法final Thread current &#61; Thread.currentThread();//这里能拿到state变量&#xff0c;这个变量是volatile修饰的&#xff0c;为的是一旦修改立即可见int c &#61; getState();//如果c为0&#xff0c;此刻表示还没有线程获取到锁if (c &#61;&#61; 0) {//hasQueuedPredecessors方法先去查看阻塞队列中是否有节点在等待&#xff0c;这里也是体现公平锁的地方if (!hasQueuedPredecessors() &&//通过CAS的方式进行设置state为1compareAndSetState(0, acquires)) {//设置独占锁的线程为当前线程setExclusiveOwnerThread(current);//返回true&#xff0c;表示获取锁成功了return true;}}//这里是可重入的体现&#xff0c;如果当前线程就是获取了独占锁的线程else if (current &#61;&#61; getExclusiveOwnerThread()) {//那么也就是将state&#43;1,也就是表示获取state&#43;1次锁int nextc &#61; c &#43; acquires;if (nextc < 0)throw new Error("Maximum lock count exceeded");setState(nextc);//此时也是获取锁成功return true;}//如果c!&#61;0并且获取锁的独占线程也不是当前线程&#xff0c;那么就返回false&#xff0c;也就是说获取锁失败了return false;}

hasQueuedPredecessors

这个方法源码虽然很短&#xff0c;但是含义却很多。

这里要先简单介绍一下这个Node&#xff1a;
这个Node里面现在只要知道里面有prev、next、thread
可以暂时理解为&#xff1a;

clss Node{//指向前一个节点Node prev;//指向下一个节点Node next;//创建当前Node的线程Thread thread;
}

Node里面当然不只是这几个属性&#xff0c;但是先这么简单的理解一下。

public final boolean hasQueuedPredecessors() {// The correctness of this depends on head being initialized// before tail and on head.next being accurate if the current// thread is first in queue.Node t &#61; tail; // Read fields in reverse initialization orderNode h &#61; head;Node s;//如果此时h&#61;&#61;t&#xff0c;说明此时队列中没有节点&#xff0c;可以是null&#61;&#61;null或者 就只有一个虚拟节点//此时如果h!&#61;t&#xff0c;说明此时队列中有至少一个节点&#xff0c;说明可能要排队return h !&#61; t &&//s表示第二个节点&#xff0c;此时如果s&#61;&#61;null&#xff0c;表示队列中只有一个节点&#xff0c;但是出现了h与t不相等的情况。//比如&#xff1a;在定义完Node s这行代码后&#xff0c;当前线程的时间片用完了&#xff0c;此时另一个线程t2开始执行//假设t2线程执行到 enq方法中的compareAndSetHead(new Node())&#xff0c;执行完了这行代码&#xff0c;然后t2的额时间片也用完了//此时又轮到t1开始执行了&#xff0c;此时h已经被初始化了&#xff0c;但是t还是null//此时h!&#61;t//但是下面这行代码&#xff0c;s&#61;&#61;null是true&#xff0c;此时说明不止当前线程在获取锁&#xff0c;并且有线程已经比当前线程更加快的执行了//此时当前线程选择加入队列&#xff0c;所以s&#61;&#61;null&#xff0c;并且这个方法返回true//如果s!&#61;null&#xff0c;那么判断当前线程是不是排队的第一个Node节点//如果当前线程不是第一个节点&#xff0c;那么当前线程的这个节点就去排队&#xff0c;//什么时候s.thread&#61;&#61;Thread.currentThread()&#xff1f;//当 当前线程是被另一个释放锁的线程unpack唤醒的时候&#xff0c;s.thread&#61;&#61;Thread.currentThread()((s &#61; h.next) &#61;&#61; null || s.thread !&#61; Thread.currentThread());}

到此&#xff0c;tryAcquire方法分析完毕。
然后返回到acquire方法。

public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}

如果当tryAcquire(arg)方法返回false&#xff0c;取反&#xff0c;那就是true----也就是获取锁失败。
此时进入第二个条件。

此时我们也可以自己简单分析到&#xff0c;现在锁被别的线程占了&#xff0c;那么当前线程应该等待锁释放&#xff0c;那么自然就应该是加入等待队列。
等待队列中&#xff0c;排队的不是线程&#xff0c;而是上面提到的Node节点。
所以接下来第一步&#xff0c;应该是得到可以代表当前thread的Node节点。
第二步&#xff0c;应该是将这个节点加入到等待队列中。
第三步&#xff0c;应该是释放CPU&#xff0c;进行等待。
接下来我们通过分析源码&#xff0c;看看是不是上面的这种思路。

addWaiter

private Node addWaiter(Node mode) {//CAS进行尾插法//如果CAS失败了&#xff0c;那么就进入enq方法进行备份//初始化新节点Node node &#61; new Node(Thread.currentThread(), mode);// Try the fast path of enq; backup to full enq on failure//记录原来的尾巴Node pred &#61; tail;if (pred !&#61; null) {//尾插法node.prev &#61; pred;//CAS进行更新尾部if (compareAndSetTail(pred, node)) {pred.next &#61; node;return node;}}//此时说明CAS失败了或者队列没有进行初始化enq(node);return node;}

此时如果上面的tail为null&#xff0c;或者CAS设置tail失败了&#xff0c;那么就要进入enq方法。

enq

/*** Inserts node into queue, initializing if necessary. See picture above.* &#64;param node the node to insert* &#64;return node&#39;s predecessor*/private Node enq(final Node node) {//此时说明CAS失败了或者队列没有进行初始化//进行自旋的CAS进行插入&#xff0c;尾插法for (;;) {Node t &#61; tail;if (t &#61;&#61; null) { // Must initialize//此时说明队列没有进行初始化&#xff0c;开始初始化if (compareAndSetHead(new Node()))tail &#61; head;} else {//CAS进行尾插法node.prev &#61; t;if (compareAndSetTail(t, node)) {t.next &#61; node;return t;}}}}

通过上面的分析&#xff0c;进入这个enq方法有两种情况。
**情况一&#xff1a;**这个等待队列还没有被初始化。那么此时的情况是这样的。如下图。
在这里插入图片描述
那么此时源码里面的t就是为null&#xff0c;此时就会 进入第一个if&#xff0c;利用CAS设置head&#xff0c;并且此时是要将head设置为一个新得Node节点。如下图。
在这里插入图片描述
此时CAS完毕&#xff0c;源码执行了tail&#61;head.如图。
在这里插入图片描述
此时设置head和tail完毕&#xff0c;也就是初始化这个队列完毕了&#xff0c;但是因为此时是出于一个死循环中&#xff0c;所以进行下一次循环。
此时还是t&#61;tail&#xff0c;不过此时t不为null&#xff0c;所以此时进入else。如图。

在这里插入图片描述
此时执行完毕。
情况二&#xff1a;是在addWaiter里面进行CAS失败了。
那么说明此时队列里面不为null&#xff0c;因为CAS失败&#xff0c;也就是有其它线程改变了tail。
那么此时的情况应该是如下图。
在这里插入图片描述

这是进入enq的时候&#xff0c;队列的情况&#xff0c;可能里面已经有了不止一个Node节点&#xff0c;可能有很多&#xff0c;图里面有一个节点表示一下&#xff0c;讲到这里&#xff0c;有个地方要补充一下。
队列初始化完以后&#xff0c;head指向的节点的thread一定为null。
所以上面 说队列中有一个节点的意思是除了head指向的那个节点以外的节点还有一个。
此时进入死循环&#xff0c;此时t&#61;tail&#xff0c;肯定不为null了&#xff0c;那么直接进入else&#xff0c;如图。
在这里插入图片描述
此时node节点也就加入了队列了。
此时enq方法返回—》addWaiter方法返回—》进入acquireQueued方法
此时要注意一下&#xff0c;addWaiter方法返回的是node节点。
到此为止和我们上面的分析差不多。
做了这么几件事&#xff0c;得到能代表当前线程的node节点&#xff0c;并且将这个节点加入到等待队列中。
那么下面应该干嘛呢&#xff1f;按道理说&#xff0c;应该是等待了&#xff0c;也就是当前线程释放cpu了。好&#xff0c;我们继续看。

acquireQueued

/*** Acquires in exclusive uninterruptible mode for thread already in* queue. Used by condition wait methods as well as acquire.** &#64;param node the node* &#64;param arg the acquire argument* &#64;return {&#64;code true} if interrupted while waiting*/final boolean acquireQueued(final Node node, int arg) {//node 即将要park的节点//真正park前&#xff0c;还要进行两次尝试获取锁boolean failed &#61; true;try {boolean interrupted &#61; false;for (;;) {//拿到node前一个节点final Node p &#61; node.predecessor();//如果此时p就是head&#xff0c;说明此时node就是排队的第一个//只有当node是排队的第一个的时候&#xff0c;才有机会去竞争锁if (p &#61;&#61; head && tryAcquire(arg)) {//这里表示竞争到了&#xff0c;将第一个节点设置为头节点&#xff0c;即虚拟节点//虚拟节点的Threa永远是nullsetHead(node);p.next &#61; null; // help GCfailed &#61; false;return interrupted;}//shouldParkAfterFailedAcquire设置p的ws为-1&#xff0c;表示p后面还有节点排队&#xff0c;并且也再给了一次机会给node去尝试获取锁if (shouldParkAfterFailedAcquire(p, node) &&//parkAndCheckInterrupt真正的去park线程&#xff0c;当前线程//parkAndCheckInterrupt返回的是当前线程的被中断状态parkAndCheckInterrupt())//如果上面parkAndCheckInterrupt返回的是true&#xff0c;说明这个线程被中断过&#xff0c;记录一下&#xff0c;后面会返回interrupted &#61; true;}} finally {if (failed)//如果获取锁失败饿了&#xff0c;也就是上面那个死循环不是获取锁程过退出的&#xff0c;而是异常推出的&#xff0c;那么就将之前插入的节点进行取消//这里的取消就是将队列中原本就是取消状态的移出队列cancelAcquire(node);}}

这里也是一个死循环。
上面说到&#xff0c;head指向的节点thread一定是null&#xff0c;那么也就是说&#xff0c;如果等到队列中有节点在等待&#xff0c;一旦释放锁&#xff0c;那么下一个得到锁的应该是head指向的那个节点的&#xff0c;下一个节点。
换句话说&#xff0c;下一次得到锁的&#xff0c;应该是prev是head的节点。
源码中

final Node p &#61; node.predecessor();if (p &#61;&#61; head && tryAcquire(arg)) {

这两行代码也就是在做这个事情&#xff0c;p就是node的前一个节点&#xff0c;如果node的节点就是head&#xff0c;那么node节点也就是在释放锁后下一个应该得到锁的节点。
所以此时当前线程又尝试去获取锁&#xff0c;可以理解为&#xff0c;我就是排队的第一个&#xff0c;我去问问&#xff0c;是不是到我了。
tryAcquire方法上面讲了&#xff0c;这里就不重复讲了。
那么这里就有两种情况&#xff0c;如果此时尝试获取锁&#xff0c;获取到了。还有就是没获取到。
情况一&#xff1a; 获取到了锁。
那么我们也可以自己分析一下。
获取到了锁&#xff0c;那下一步&#xff0c;应该是出队。
不过这里的具体实现的做法是&#xff1a;
将当前节点设置为head&#xff0c;也就是setHead方法。可以进去看一下&#xff1a;

private void setHead(Node node) {head &#61; node;node.thread &#61; null;node.prev &#61; null;}

可以看到&#xff0c;修改了head为node ,并且当thread置为null&#xff0c;也就应了上面说的&#xff0c;head指向的节点中的thread一定为null。然后prev置为null&#xff0c;因为此时node就是head&#xff0c;所以prev肯定就null了。
然后p.next&#61;null也就是将之前的头节点的next指针指向null。
源代码的作者写的注释是help GC。
然后修改了一个变量failed为false。这说明获取锁成功了。
这里返回的interrupted是个boolean类型&#xff0c;这个-------------------
情况二&#xff1a; 尝试获取锁失败了
那么应该进入下一个if

shouldParkAfterFailedAcquire&#xff08;第一次&#xff09;

/*** Checks and updates status for a node that failed to acquire.* Returns true if thread should block. This is the main signal* control in all acquire loops. Requires that pred &#61;&#61; node.prev.** &#64;param pred node&#39;s predecessor holding status* &#64;param node the node* &#64;return {&#64;code true} if thread should block*/private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {//获取前一个节点的等待标志//第一次进来&#xff0c;这个waiteStatus是Node里面的一个属性&#xff0c;不过从上面源码一直到这里&#xff0c;我们可没有设置过&#xff0c;所以他就是初始化的值0,这里要注意&#xff0c;这个ws拿的是pred的&#xff0c;也就是前一个节点的。int ws &#61; pred.waitStatus;//Node.SIGNAL为-1 如果前一个状态为-1&#xff0c;那么线程这个线程真的要被park了。//第一次进来&#xff0c;ws为0&#xff0c;不会进这个ifif (ws &#61;&#61; Node.SIGNAL)/** This node has already set status asking a release* to signal it, so it can safely park.*/return true;//第一次进来&#xff0c;ws为0,不会进这个ifif (ws > 0) {/** Predecessor was cancelled. Skip over predecessors and* indicate retry.*///说明前一个节点被取消了&#xff0c;那么更新要给这个队列do {node.prev &#61; pred &#61; pred.prev;} while (pred.waitStatus > 0);pred.next &#61; node;} else {/** waitStatus must be 0 or PROPAGATE. Indicate that we* need a signal, but don&#39;t park yet. Caller will need to* retry to make sure it cannot acquire before parking.*///设置前一个节点的等待状态//第一次进来&#xff0c;因为ws为0&#xff0c;不会进入上面的条件&#xff0c;所以进到这里//此时是将前一个节点的WaitStatus设置为-1&#xff0c;也就是Node.SIGNALcompareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;}

然后此时shouldParkAfterFailedAcquire方法退出&#xff0c;返回false。
此时也就是返回到了acquireQueued。

if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted &#61; true;

此时这个if第一个条件返回了false&#xff0c;所以不会继续判断第二个条件&#xff0c;但是当前是处在一个死循环中&#xff0c;所以进行下一轮循环。
又来一遍&#xff0c;获取当前节点的前一个节点p&#xff0c;然后再判断前一个节点是不是head&#xff0c;如果是&#xff0c;那就再去尝试获取一次锁。。。。。。
然后又到了这个shouldParkAfterFailedAcquire

shouldParkAfterFailedAcquire&#xff08;第二次&#xff09;

/*** Checks and updates status for a node that failed to acquire.* Returns true if thread should block. This is the main signal* control in all acquire loops. Requires that pred &#61;&#61; node.prev.** &#64;param pred node&#39;s predecessor holding status* &#64;param node the node* &#64;return {&#64;code true} if thread should block*/private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {//获取前一个节点的等待标志//第一次进来&#xff0c;这个waiteStatus是Node里面的一个属性&#xff0c;不过从上面源码一直到这里&#xff0c;我们可没有设置过&#xff0c;所以他就是初始化的值0,这里要注意&#xff0c;这个ws拿的是pred的&#xff0c;也就是前一个节点的。int ws &#61; pred.waitStatus;//Node.SIGNAL为-1 如果前一个状态为-1&#xff0c;那么线程这个线程真的要被park了。//第一次进来&#xff0c;ws为0&#xff0c;不会进这个if//第二次进来&#xff0c;因为第一次已经将pred.waitStatus设置为了Node.SIGNAL&#xff0c;所以此时会返回trueif (ws &#61;&#61; Node.SIGNAL)/** This node has already set status asking a release* to signal it, so it can safely park.*/return true;//第一次进来&#xff0c;ws为0,不会进这个ifif (ws > 0) {/** Predecessor was cancelled. Skip over predecessors and* indicate retry.*///说明前一个节点被取消了&#xff0c;那么更新要给这个队列do {node.prev &#61; pred &#61; pred.prev;} while (pred.waitStatus > 0);pred.next &#61; node;} else {/** waitStatus must be 0 or PROPAGATE. Indicate that we* need a signal, but don&#39;t park yet. Caller will need to* retry to make sure it cannot acquire before parking.*///设置前一个节点的等待状态//第一次进来&#xff0c;因为ws为0&#xff0c;不会进入上面的条件&#xff0c;所以进到这里//此时是将前一个节点的WaitStatus设置为-1&#xff0c;也就是Node.SIGNALcompareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;}

第二次进来&#xff0c;直接进了第一个if&#xff0c;返回true.那么此时又返回到acquireQueued。

if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted &#61; true;

此时第一个条件返回了true&#xff0c;那么继续判断第二个条件。

parkAndCheckInterrupt

private final boolean parkAndCheckInterrupt() {//此时park了&#xff0c;释放了cpu&#xff0c;下一次被唤醒也是从这里开始继续执行&#xff0c;这点要记得。LockSupport.park(this);return Thread.interrupted();}

到此&#xff0c;lock成功与不成功分析完毕。

这里再说一个&#xff0c;就是这个Thread.isinterrupted方法。
可以继续跟一下。

/*** Tests whether the current thread has been interrupted. The* interrupted status of the thread is cleared by this method. In* other words, if this method were to be called twice in succession, the* second call would return false (unless the current thread were* interrupted again, after the first call had cleared its interrupted* status and before the second call had examined it).**

A thread interruption ignored because a thread was not alive* at the time of the interrupt will be reflected by this method* returning false.** &#64;return true if the current thread has been interrupted;* false otherwise.* &#64;see #isInterrupted()* &#64;revised 6.0*/public static boolean interrupted() {return currentThread().isInterrupted(true);}

再根就是一个nvtive方法了&#xff0c;不过这里的注释写的很清楚&#xff0c;这个方法实现了&#xff1a;
返回当前线程是否处于中断状态。
如果处于中断状态&#xff0c;那么这个状态会被重置为运行状态。返回true。
如果处于运行状态&#xff0c;直接返回false。

说到这里&#xff0c;就不得不再说一个和它类似的方法了。
thread.isInterrupted()
这个方法跟一下&#xff1a;

/*** Tests whether this thread has been interrupted. The interrupted* status of the thread is unaffected by this method.**

A thread interruption ignored because a thread was not alive* at the time of the interrupt will be reflected by this method* returning false.** &#64;return true if this thread has been interrupted;* false otherwise.* &#64;see #interrupted()* &#64;revised 6.0*/public boolean isInterrupted() {return isInterrupted(false);}

这个方法就是单纯的返回线程是否处于中断状态。不会去清除中断状态。


推荐阅读
  • 重入锁(ReentrantLock)学习及实现原理
    本文介绍了重入锁(ReentrantLock)的学习及实现原理。在学习synchronized的基础上,重入锁提供了更多的灵活性和功能。文章详细介绍了重入锁的特性、使用方法和实现原理,并提供了类图和测试代码供读者参考。重入锁支持重入和公平与非公平两种实现方式,通过对比和分析,读者可以更好地理解和应用重入锁。 ... [详细]
  • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
  • JVM 学习总结(三)——对象存活判定算法的两种实现
    本文介绍了垃圾收集器在回收堆内存前确定对象存活的两种算法:引用计数算法和可达性分析算法。引用计数算法通过计数器判定对象是否存活,虽然简单高效,但无法解决循环引用的问题;可达性分析算法通过判断对象是否可达来确定存活对象,是主流的Java虚拟机内存管理算法。 ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • 本文讨论了在openwrt-17.01版本中,mt7628设备上初始化启动时eth0的mac地址总是随机生成的问题。每次随机生成的eth0的mac地址都会写到/sys/class/net/eth0/address目录下,而openwrt-17.01原版的SDK会根据随机生成的eth0的mac地址再生成eth0.1、eth0.2等,生成后的mac地址会保存在/etc/config/network下。 ... [详细]
  • JDK源码学习之HashTable(附带面试题)的学习笔记
    本文介绍了JDK源码学习之HashTable(附带面试题)的学习笔记,包括HashTable的定义、数据类型、与HashMap的关系和区别。文章提供了干货,并附带了其他相关主题的学习笔记。 ... [详细]
  • Android系统源码分析Zygote和SystemServer启动过程详解
    本文详细解析了Android系统源码中Zygote和SystemServer的启动过程。首先介绍了系统framework层启动的内容,帮助理解四大组件的启动和管理过程。接着介绍了AMS、PMS等系统服务的作用和调用方式。然后详细分析了Zygote的启动过程,解释了Zygote在Android启动过程中的决定作用。最后通过时序图展示了整个过程。 ... [详细]
  • 本文整理了Java面试中常见的问题及相关概念的解析,包括HashMap中为什么重写equals还要重写hashcode、map的分类和常见情况、final关键字的用法、Synchronized和lock的区别、volatile的介绍、Syncronized锁的作用、构造函数和构造函数重载的概念、方法覆盖和方法重载的区别、反射获取和设置对象私有字段的值的方法、通过反射创建对象的方式以及内部类的详解。 ... [详细]
  • 本文介绍了解决二叉树层序创建问题的方法。通过使用队列结构体和二叉树结构体,实现了入队和出队操作,并提供了判断队列是否为空的函数。详细介绍了解决该问题的步骤和流程。 ... [详细]
  • Tomcat/Jetty为何选择扩展线程池而不是使用JDK原生线程池?
    本文探讨了Tomcat和Jetty选择扩展线程池而不是使用JDK原生线程池的原因。通过比较IO密集型任务和CPU密集型任务的特点,解释了为何Tomcat和Jetty需要扩展线程池来提高并发度和任务处理速度。同时,介绍了JDK原生线程池的工作流程。 ... [详细]
  • 深入理解Kafka服务端请求队列中请求的处理
    本文深入分析了Kafka服务端请求队列中请求的处理过程,详细介绍了请求的封装和放入请求队列的过程,以及处理请求的线程池的创建和容量设置。通过场景分析、图示说明和源码分析,帮助读者更好地理解Kafka服务端的工作原理。 ... [详细]
  • 李逍遥寻找仙药的迷阵之旅
    本文讲述了少年李逍遥为了救治婶婶的病情,前往仙灵岛寻找仙药的故事。他需要穿越一个由M×N个方格组成的迷阵,有些方格内有怪物,有些方格是安全的。李逍遥需要避开有怪物的方格,并经过最少的方格,找到仙药。在寻找的过程中,他还会遇到神秘人物。本文提供了一个迷阵样例及李逍遥找到仙药的路线。 ... [详细]
  • Android工程师面试准备及设计模式使用场景
    本文介绍了Android工程师面试准备的经验,包括面试流程和重点准备内容。同时,还介绍了建造者模式的使用场景,以及在Android开发中的具体应用。 ... [详细]
  • 基于Socket的多个客户端之间的聊天功能实现方法
    本文介绍了基于Socket的多个客户端之间实现聊天功能的方法,包括服务器端的实现和客户端的实现。服务器端通过每个用户的输出流向特定用户发送消息,而客户端通过输入流接收消息。同时,还介绍了相关的实体类和Socket的基本概念。 ... [详细]
  • 本文介绍了在Android开发中使用软引用和弱引用的应用。如果一个对象只具有软引用,那么只有在内存不够的情况下才会被回收,可以用来实现内存敏感的高速缓存;而如果一个对象只具有弱引用,不管内存是否足够,都会被垃圾回收器回收。软引用和弱引用还可以与引用队列联合使用,当被引用的对象被回收时,会将引用加入到关联的引用队列中。软引用和弱引用的根本区别在于生命周期的长短,弱引用的对象可能随时被回收,而软引用的对象只有在内存不够时才会被回收。 ... [详细]
author-avatar
1157476701qq
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有