转载文章,拜读了一下原文感觉很不错,转载一下,侵删

链接地址:http://objcoding.com/2019/05/05/aqs-exclusive-lock/

Java并发之AQS源码分析(一)

2019-05-05
zch
 

AQS 全称是 AbstractQueuedSynchronizer,顾名思义,是一个用来构建锁和同步器的框架,它底层用了 CAS 技术来保证操作的原子性,同时利用 FIFO 队列实现线程间的锁竞争,将基础的同步相关抽象细节放在 AQS,这也是 ReentrantLock、CountDownLatch 等同步工具实现同步的底层实现机制。它能够成为实现大部分同步需求的基础,也是 J.U.C 并发包同步的核心基础组件。

AQS 结构剖析

AQS 就是建立在 CAS 的基础之上,增加了大量的实现细节,例如获取同步状态、FIFO 同步队列,独占式锁和共享式锁的获取和释放等等,这些都是 AQS 类对于同步操作抽离出来的一些通用方法,这么做也是为了对实现的一个同步类屏蔽了大量的细节,大大降低了实现同步工具的工作量,这也是为什么 AQS 是其它许多同步类的基类的原因。

现在我们来直接定位到类 java.util.concurrent.locks.AbstractQueuedSynchronizer,下面是 AQS 类的几个重要字段与方法列出来:

  1. public abstract class AbstractQueuedSynchronizer
  2. extends AbstractOwnableSynchronizer
  3. implements java.io.Serializable {
  4. private transient volatile Node head;
  5. private transient volatile Node tail;
  6. private volatile int state;
  7. protected final boolean compareAndSetState(int expect, int update) {
  8. // See below for intrinsics setup to support this
  9. return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
  10. }
  11. // ...
  12. }
  1. head 字段为等待队列的头节点,表示当前正在执行的节点;
  2. tail 字段为等待队列的尾节点;
  3. state 字段为同步状态,其中 state > 0 为有锁状态,每次加锁就在原有 state 基础上加 1,即代表当前持有锁的线程加了 state 次锁,反之解锁时每次减一,当 statte = 0 为无锁状态;
  4. 通过 compareAndSetState 方法操作 CAS 更改 state 状态,保证 state 的原子性。

有没有发现,这几个字段都用 volatile 关键字进行修饰,以确保多线程间保证字段的可见性。

AQS 提供了两种锁,分别是独占锁和共享锁,独占锁指的是操作被认作一种独占操作,比如 ReentrantLock,它实现了独占锁的方法,而共享锁则指的是一个非独占操作,比如一些同步工具 CountDownLatch 和 Semaphore 等同步工具,下面是 AQS 对这两种锁提供的抽象方法。

独占锁:

  1. // 获取锁方法
  2. protected boolean tryAcquire(int arg) {
  3. throw new UnsupportedOperationException();
  4. }
  5. // 释放锁方法
  6. protected boolean tryRelease(int arg) {
  7. throw new UnsupportedOperationException();
  8. }

共享锁:

  1. // 获取锁方法
  2. protected int tryAcquireShared(int arg) {
  3. throw new UnsupportedOperationException();
  4. }
  5. // 释放锁方法
  6. protected boolean tryReleaseShared(int arg) {
  7. throw new UnsupportedOperationException();
  8. }

在我们平时开发中,基本不用直接使用 AQS,我们平时都是直接使用 JDK 自带的同步类工具,如 ReentrantLock、CountDownLatch 和 Semaphore 等,它们已经可以满足绝大部分的需求了,后面会抽几篇文章单独讲一下这些同步类工具是如何使用 AQS 的,这对于我们如何构建自定义的同步工具,有很大的帮助。

下面是同步队列节点的结构:

用大神的注释来形象地描述一下队列的模型:

  1. /**
  2. * <pre>
  3. * +------+ prev +-----+ +-----+
  4. * head | | <---- | | <---- | | tail
  5. * +------+ +-----+ +-----+
  6. * </pre>
  7. */

这是一个普通双向链表的节点结构,多了 thread 字段用于存储当前线程对象,同时每个节点都有一个 waitStatus 等待状态,一共有四种状态:

  1. CANCELLED(1):取消状态,如果当前线程的前置节点状态为 CANCELLED,则表明前置节点已经等待超时或者已经被中断了,这时需要将其从等待队列中删除。
  2. SIGNAL(-1):等待触发状态,如果当前线程的前置节点状态为 SIGNAL,则表明当前线程需要阻塞。
  3. CONDITION(-2):等待条件状态,表示当前节点在等待 condition,即在 condition 队列中。
  4. PROPAGATE(-3):状态需要向后传播,表示 releaseShared 需要被传播给后续节点,仅在共享锁模式下使用。

可以这么理解:head 节点可以表示成当前持有锁的线程的节点,其余线程竞争锁失败后,会加入到队尾,tail 始终指向队列的最后一个节点。

AQS 的结构大概可总结为以下 3 部分:

  1. 用 volatile 修饰的整数类型的 state 状态,用于表示同步状态,提供 getState 和 setState 来操作同步状态;
  2. 提供了一个 FIFO 等待队列,实现线程间的竞争和等待,这是 AQS 的核心;
  3. AQS 内部提供了各种基于 CAS 原子操作方法,如 compareAndSetState 方法,并且提供了锁操作的acquire和release方法。

独占锁

独占锁的原理是如果有线程获取到锁,那么其它线程只能是获取锁失败,然后进入等待队列中等待被唤醒。

获取锁

获取独占锁方法:

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

源码解读:

  1. 通过 tryAcquire(arg) 方法尝试获取锁,这个方法需要实现类自己实现获取锁的逻辑,获取锁成功后则不执行后面加入等待队列的逻辑了;
  2. 如果尝试获取锁失败后,则执行 addWaiter(Node.EXCLUSIVE) 方法将当前线程封装成一个 Node 节点对象,并加入队列尾部;
  3. 把当前线程执行封装成 Node 节点后,继续执行 acquireQueued 的逻辑,该逻辑主要是判断当前节点的前置节点是否是头节点,来尝试获取锁,如果获取锁成功,则当前节点就会成为新的头节点,这也是获取锁的核心逻辑。

基于上面源码的步骤分析后,我们一步步往下看源码具体实现:

  1. private Node addWaiter(Node mode) {
  2. // 创建一个基于当前线程的节点,该节点是 Node.EXCLUSIVE 独占式类型
  3. Node node = new Node(Thread.currentThread(), mode);
  4. // Try the fast path of enq; backup to full enq on failure
  5. Node pred = tail;
  6. // 这里先判断队尾是否为空,如果不为空则直接将节点加入队尾
  7. if (pred != null) {
  8. node.prev = pred;
  9. // 采取 CAS 操作,将当前节点设置为队尾节点,由于采用了 CAS 原子操作,无论并发怎么修改,都有且只有一条线程可以修改成功,其余都将执行后面的enq方法
  10. if (compareAndSetTail(pred, node)) {
  11. pred.next = node;
  12. return node;
  13. }
  14. }
  15. enq(node);
  16. return node;
  17. }

简单来说 addWaiter(Node mode) 方法做了以下事情:

  1. 创建基于当前线程的独占式类型的节点;
  2. 利用 CAS 原子操作,将节点加入队尾。

我们继续看 enq(Node node) 方法:

  1. private Node enq(final Node node) {
  2. // 自旋操作
  3. for (;;) {
  4. Node t = tail;
  5. // 如果队尾节点为空,那么进行CAS操作初始化队列
  6. if (t == null) {
  7. // 这里很关键,即如果队列为空,那么此时必须初始化队列,初始化一个空的节点表示队列头,用于表示当前正在执行的节点,头节点即表示当前正在运行的节点
  8. if (compareAndSetHead(new Node()))
  9. tail = head;
  10. } else {
  11. node.prev = t;
  12. // 这一步也是采取CAS操作,将当前节点加入队尾,如果失败的话,自旋继续修改直到成功为止
  13. if (compareAndSetTail(t, node)) {
  14. t.next = node;
  15. return t;
  16. }
  17. }
  18. }
  19. }

enq(final Node node) 方法主要做了以下事情:

  1. 采用自旋机制,这是 aqs 里面很重要的一个机制;
  2. 如果队尾节点为空,则初始化队列,将头节点设置为空节点,头节点即表示当前正在运行的节点;
  3. 如果队尾节点不为空,则继续采取 CAS 操作,将当前节点加入队尾,不成功则继续自旋,直到成功为止;

对比了上面两段代码,不难看出,首先是判断队尾是否为空,先进行一次 CAS 入队操作,如果失败则进入 enq(final Node node) 方法执行完整的入队操作。

完整的入队操作简单来说就是:如果队列为空,初始化队列,并将头节点设为空节点,表示当前正在运行的节点,然后再将当前线程的节点加入到队列尾部。

关于队列的初始化与入队,务必理解透彻。

经过上面 CAS 不断尝试,这时当前节点已经成功加入到队尾了,接下来就到了acquireQueued 的逻辑,我们继续往下看源码:

  1. final boolean acquireQueued(final Node node, int arg) {
  2. boolean failed = true;
  3. try {
  4. // 线程中断标记字段
  5. boolean interrupted = false;
  6. for (;;) {
  7. // 获取当前节点的 pred 节点
  8. final Node p = node.predecessor();
  9. // 如果 pred 节点为 head 节点,那么再次尝试获取锁
  10. if (p == head && tryAcquire(arg)) {
  11. // 获取锁之后,那么当前节点也就成为了 head 节点
  12. setHead(node);
  13. p.next = null; // help GC
  14. failed = false;
  15. // 不需要挂起,返回 false
  16. return interrupted;
  17. }
  18. // 获取锁失败,则进入挂起逻辑
  19. if (shouldParkAfterFailedAcquire(p, node) &&
  20. parkAndCheckInterrupt())
  21. interrupted = true;
  22. }
  23. } finally {
  24. if (failed)
  25. cancelAcquire(node);
  26. }
  27. }

这一步 acquireQueued(final Node node, int arg) 方法主要做了以下事情:

  1. 判断当前节点的 pred 节点是否为 head 节点,如果是,则尝试获取锁;
  2. 获取锁失败后,进入挂起逻辑。

提醒一点:我们上面也说过,head 节点代表当前持有锁的线程,那么如果当前节点的 pred 节点是 head 节点,很可能此时 head 节点已经释放锁了,所以此时需要再次尝试获取锁。

接下来继续看挂起逻辑源码:

  1. private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
  2. int ws = pred.waitStatus;
  3. if (ws == Node.SIGNAL)
  4. // 如果 pred 节点为 SIGNAL 状态,返回true,说明当前节点需要挂起
  5. return true;
  6. // 如果ws > 0,说明节点状态为CANCELLED,需要从队列中删除
  7. if (ws > 0) {
  8. do {
  9. node.prev = pred = pred.prev;
  10. } while (pred.waitStatus > 0);
  11. pred.next = node;
  12. } else {
  13. // 如果是其它状态,则操作CAS统一改成SIGNAL状态
  14. // 由于这里waitStatus的值只能是0或者PROPAGATE,所以我们将节点设置为SIGNAL,从新循环一次判断
  15. compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
  16. }
  17. return false;
  18. }

这一步 shouldParkAfterFailedAcquire(Node pred, Node node) 方法主要做了以下事情:

  1. 判断 pred 节点状态,如果为 SIGNAL 状态,则直接返回 true 执行挂起;
  2. 删除状态为 CANCELLED 的节点;
  3. 若 pred 节点状态为 0 或者 PROPAGATE,则将其设置为为 SIGNAL,再从 acquireQueued 方法自旋操作从新循环一次判断。

通俗来说就是:根据 pred 节点状态来判断当前节点是否可以挂起,如果该方法返回 false,那么挂起条件还没准备好,就会重新进入 acquireQueued(final Node node, int arg) 的自旋体,重新进行判断。如果返回 true,那就说明当前线程可以进行挂起操作了,那么就会继续执行挂起。

这里需要注意的时候,节点的初始值为 0,因此如果获取锁失败,会尝试将节点设置为 SIGNAL。

继续看挂起逻辑:

  1. private final boolean parkAndCheckInterrupt() {
  2. LockSupport.park(this);
  3. return Thread.interrupted();
  4. }

LockSupport 是用来创建锁和其他同步类的基本线程阻塞原语。LockSupport 提供 park() 和 unpark() 方法实现阻塞线程和解除线程阻塞。release 释放锁方法逻辑会调用 LockSupport.unPark 方法来唤醒后继节点。

获取独占锁流程图:

释放锁

释放锁方法:

  1. public final boolean release(int arg) {
  2. if (tryRelease(arg)) {
  3. Node h = head;
  4. if (h != null && h.waitStatus != 0)
  5. unparkSuccessor(h);
  6. return true;
  7. }
  8. return false;
  9. }

释放锁的方法源码就很好理解,通过 tryRelease(arg) 方法尝试释放锁,这个方法需要实现类自己实现释放锁的逻辑,释放锁成功后则执行后面的唤醒后续节点的逻辑了,然后判断 head 节点不为空并且 head 节点状态不为 0,因为 addWaiter 方法默认的节点状态为 0,此时节点还没有进入就绪状态。

继续往下看源码:

  1. private void unparkSuccessor(Node node) {
  2. int ws = node.waitStatus;
  3. if (ws < 0)
  4. // 将头节点的状态设置为0
  5. // 这里会尝试清除头节点的状态,改为初始状态
  6. compareAndSetWaitStatus(node, ws, 0);
  7. // 后继节点
  8. Node s = node.next;
  9. // 如果后继节点为null,或者已经被取消了
  10. if (s == null || s.waitStatus > 0) {
  11. s = null;
  12. // for循环从队列尾部一直往前找可以唤醒的节点
  13. for (Node t = tail; t != null && t != node; t = t.prev)
  14. if (t.waitStatus <= 0)
  15. s = t;
  16. }
  17. if (s != null)
  18. // 唤醒后继节点
  19. LockSupport.unpark(s.thread);
  20. }

从源码可看出:释放锁主要是将头节点的后继节点唤醒,如果后继节点不符合唤醒条件,则从队尾一直往前找,直到找到符合条件的节点为止

总结

这篇文章主要讲述了 AQS 的内部结构和它的同步实现原理,并从源码的角度深度剖析了 AQS 独占锁模式下的获取锁与释放锁的逻辑,并且从源码中我们得出:在独占锁模式下,用 state 值表示锁并且 0 表示无锁状态,0 -> 1 表示从无锁到有锁,仅允许一条线程持有锁,其余的线程会被包装成一个 Node 节点放到队列中进行挂起,队列中的头节点表示当前正在执行的线程,当头节点释放后会唤醒后继节点,从而印证了 AQS 的队列是一个 FIFO 同步队列。

Java并发之AQS源码分析(二)

2019-05-08
zch
 

我在Java并发之AQS源码分析(一)这篇文章中,从源码的角度深度剖析了 AQS 独占锁模式下的获取锁与释放锁的逻辑,如果你把这部分搞明白了,再看共享锁的实现原理,思路就会清晰很多。下面我们继续从源码中窥探共享锁的实现原理。

共享锁

获取锁

  1. public final void acquireShared(int arg) {
  2. // 尝试获取共享锁,小于0表示获取失败
  3. if (tryAcquireShared(arg) < 0)
  4. // 执行获取锁失败的逻辑
  5. doAcquireShared(arg);
  6. }

这里的 tryAcquireShared 方法是留给实现方去实现获取锁的具体逻辑的,我们主要看 doAcquireShared 方法的实现逻辑:

  1. private void doAcquireShared(int arg) {
  2. // 添加共享锁类型节点到队列中
  3. final Node node = addWaiter(Node.SHARED);
  4. boolean failed = true;
  5. try {
  6. boolean interrupted = false;
  7. for (;;) {
  8. final Node p = node.predecessor();
  9. if (p == head) {
  10. // 再次尝试获取共享锁
  11. int r = tryAcquireShared(arg);
  12. // 如果在这里成功获取共享锁,会进入共享锁唤醒逻辑
  13. if (r >= 0) {
  14. // 共享锁唤醒逻辑
  15. setHeadAndPropagate(node, r);
  16. p.next = null; // help GC
  17. if (interrupted)
  18. selfInterrupt();
  19. failed = false;
  20. return;
  21. }
  22. }
  23. // 与独占锁相同的挂起逻辑
  24. if (shouldParkAfterFailedAcquire(p, node) &&
  25. parkAndCheckInterrupt())
  26. interrupted = true;
  27. }
  28. } finally {
  29. if (failed)
  30. cancelAcquire(node);
  31. }
  32. }

看到上面的代码,是不是有一种熟悉的感觉,同样是采用了自旋机制,在线程挂起之前,不断地循环尝试获取锁,不同的是,一旦获取共享锁,会调用 setHeadAndPropagate 方法同时唤醒后继节点,实现共享模式,下面是唤醒后继节点代码逻辑:

  1. private void setHeadAndPropagate(Node node, int propagate) {
  2. // 头节点
  3. Node h = head;
  4. // 设置当前节点为新的头节点
  5. // 这里不需要加锁操作,因为获取共享锁后,会从FIFO队列中依次唤醒队列,并不会产生并发安全问题
  6. setHead(node);
  7. if (propagate > 0 || h == null || h.waitStatus < 0 ||
  8. (h = head) == null || h.waitStatus < 0) {
  9. // 后继节点
  10. Node s = node.next;
  11. // 如果后继节点为空或者后继节点为共享类型,则进行唤醒后继节点
  12. // 这里后继节点为空意思是只剩下当前头节点了
  13. if (s == null || s.isShared())
  14. doReleaseShared();
  15. }
  16. }

该方法主要做了两个重要的步骤:

  1. 将当前节点设置为新的头节点,这点很重要,这意味着当前节点的前置节点(旧头节点)已经获取共享锁了,从队列中去除;
  2. 调用 doReleaseShared 方法,它会调用 unparkSuccessor 方法唤醒后继节点。

释放锁

  1. public final boolean releaseShared(int arg) {
  2. // 由用户自行实现释放锁条件
  3. if (tryReleaseShared(arg)) {
  4. // 执行释放锁
  5. doReleaseShared();
  6. return true;
  7. }
  8. return false;
  9. }

下面是释放锁逻辑:

  1. private void doReleaseShared() {
  2. for (;;) {
  3. // 从头节点开始执行唤醒操作
  4. // 这里需要注意,如果从setHeadAndPropagate方法调用该方法,那么这里的head是新的头节点
  5. Node h = head;
  6. if (h != null && h != tail) {
  7. int ws = h.waitStatus;
  8. //表示后继节点需要被唤醒
  9. if (ws == Node.SIGNAL) {
  10. // 初始化节点状态
  11. //这里需要CAS原子操作,因为setHeadAndPropagate和releaseShared这两个方法都会顶用doReleaseShared,避免多次unpark唤醒操作
  12. if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
  13. // 如果初始化节点状态失败,继续循环执行
  14. continue; // loop to recheck cases
  15. // 执行唤醒操作
  16. unparkSuccessor(h);
  17. }
  18. //如果后继节点暂时不需要唤醒,那么当前头节点状态更新为PROPAGATE,确保后续可以传递给后继节点
  19. else if (ws == 0 &&
  20. !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
  21. continue; // loop on failed CAS
  22. }
  23. // 如果在唤醒的过程中头节点没有更改,退出循环
  24. // 这里防止其它线程又设置了头节点,说明其它线程获取了共享锁,会继续循环操作
  25. if (h == head) // loop if head changed
  26. break;
  27. }
  28. }

共享锁的释放锁逻辑比独占锁的释放锁逻辑稍微复杂,原因是共享锁需要释放队列中所有共享类型的节点,因此需要循环操作,由于释放锁过程中会涉及多个地方修改节点状态,此时需要 CAS 原子操作来并发安全。

获取共享锁流程图:

总结

跟独占锁相比,从流程图也可看出,共享锁的主要特征是当有一个线程获取到锁之后,那么它就会依次唤醒等待队列中可以跟它共享的节点,当然这些节点也是共享锁类型。

AbstractQueuedSynchronizer 源码解读(转载)的更多相关文章

  1. AbstractQueuedSynchronizer源码解读

    1. 背景 AQS(java.util.concurrent.locks.AbstractQueuedSynchronizer)是Doug Lea大师创作的用来构建锁或者其他同步组件(信号量.事件等) ...

  2. AbstractQueuedSynchronizer源码解读--续篇之Condition

    1. 背景 在之前的AbstractQueuedSynchronizer源码解读中,介绍了AQS的基本概念.互斥锁.共享锁.AQS对同步队列状态流转管理.线程阻塞与唤醒等内容.其中并不涉及Condit ...

  3. AbstractQueuedSynchronizer源码分析

    AbstractQueuedSynchronizer源码分析 前提 AQS(java.util.concurrent.locks.AbstractQueuedSynchronizer)是并发编程大师D ...

  4. AFNetworking 3.0 源码解读 总结(干货)(上)

    养成记笔记的习惯,对于一个软件工程师来说,我觉得很重要.记得在知乎上看到过一个问题,说是人类最大的缺点是什么?我个人觉得记忆算是一个缺点.它就像时间一样,会自己消散. 前言 终于写完了 AFNetwo ...

  5. AFNetworking 3.0 源码解读(三)之 AFURLRequestSerialization

    这篇就讲到了跟请求相关的类了 关于AFNetworking 3.0 源码解读 的文章篇幅都会很长,因为不仅仅要把代码进行详细的的解释,还会大概讲解和代码相关的知识点. 上半篇: URI编码的知识 关于 ...

  6. AFNetworking 3.0 源码解读 总结

    终于写完了 AFNetworking 的源码解读.这一过程耗时数天.当我回过头又重头到尾的读了一篇,又有所收获.不禁让我想起了当初上学时的种种情景.我们应该对知识进行反复的记忆和理解.下边是我总结的 ...

  7. jQuery.Callbacks 源码解读二

    一.参数标记 /* * once: 确保回调列表仅只fire一次 * unique: 在执行add操作中,确保回调列表中不存在重复的回调 * stopOnFalse: 当执行回调返回值为false,则 ...

  8. QCustomplot使用分享(二) 源码解读

    一.头文件概述 从这篇文章开始,我们将正式的进入到QCustomPlot的实践学习中来,首先我们先来学习下QCustomPlot的类图,如果下载了QCustomPlot源码的同学可以自己去QCusto ...

  9. seajs 源码解读

    之前面试时老问一个问题seajs 是怎么加载js 文件的 在网上找一些资料,觉得这个写的不错就转载了,记录一下,也学习一下 seajs 源码解读 seajs 简单介绍 seajs是前端应用模块化开发的 ...

随机推荐

  1. 程序装载:“640K内存”真的不够用么?

    本文源于size_t的说明,看到比尔盖茨说过:640K内存对于任何人来说都足够了,所以找了一篇文章,学习一下~~ 一直以来都知道自己有关计算机底层的知识不是不扎实,前段时间跟着大佬们推荐在[极客时间] ...

  2. BZOJ 3566 概率充电器(树形概率DP)

    题面 题目传送门 分析 定义f(i)f(i)f(i)为iii点不被点亮的概率,p(i)p(i)p(i)为iii自己被点亮的概率,p(i,j)p(i,j)p(i,j)表示i−ji-ji−j 这条边联通的 ...

  3. 谷歌浏览器不兼容的一些Js

    这篇博文主要记录本人在实际应用中碰到的谷歌浏览器与一些Js不兼容的问题,随着时间的推移,这篇博文的内容可能越来越多,也可能一点也没有(我想那时候谷歌肯定是相当牛逼的). 1.谷歌浏览器不兼容docum ...

  4. Docker搭建Redis一主两从三哨兵

    作者:oscarwin juejin.im/post/5d26b03de51d454fa33b1960 这次实验准备了三台云主机,系统为Debian,ip分别为:35.236.172.131 ,35. ...

  5. 002_STM32程序移植之_DHT11

    1. 测试环境:STM32C8T6 2. 测试模块:DHT11温湿度模块 3. 测试接口: 1. DHT11温湿度模块接口: DS1302引脚 ---------单片机引脚 VCC---------- ...

  6. MySQL组提交(group commit)

    MySQL组提交(group commit) 前提: 以下讨论的前提 是设置MySQL的crash safe相关参数为双1: sync_binlog=1 innodb_flush_log_at_trx ...

  7. redis数据存储--redis在Windows下的安装过程

    一.下载软件 1. 下载Redis windows版本,Redis官网下载地址为:https://redis.io/download: 这里下载的是Windows版本,下载地址为:https://gi ...

  8. Python数据类型之数值-Python基础前传(5)

    学习任何一门学科或者手艺,最忌讳的就是想的太多,做的太少: 有很多朋友私信问我:jacky,我们该如何选择Python的课程?或是我们该如何选择Mysql课程?到底谁的课件和书籍才是最好的? 借着今天 ...

  9. 使用setUncaughtExceptionHandler在线程外面捕获异常

    package com.dwz.concurrency.chapter11; /** * Thread的run方法是不能throw出异常的,只能在日志或者console中打印出来 */ public ...

  10. go语言学习笔记之数组

    package main import ( "fmt" ) func main() { // Declare arrays var x[5] int //Assign value ...