最近工作不饱和,写写文章充充电。何以解忧,唯有Coding。后续更新的文章涉及的方向有:ThreadPoolExecutor、Spring、MyBatis、ReentrantLock、CyclicBarrier、Semaphore.

同系列文章: 1.看ThreadPoolExecutor源码前的骚操作


开始讲解之前,自定义ThreadPoolExecutor和Task。

public class CustomThreadPoolExecutor extends ThreadPoolExecutor {public CustomThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);}public static class CustomTask<V> extends FutureTask<V> {public CustomTask(Callable<V> callable) {super(callable);}public CustomTask(Runnable runnable, V result) {super(runnable, result);}}}
复制代码
  • 为什么要需要线程池 ? 线程资源必须通过线程池提供,不允许在应用中自行显式的创建线程。使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池的话,有可能造成系统创建大量的同类线程而消耗完线程而导致消耗完内存或者过度切换的问题。

  • 为什么要不能通过Executors去创建线程池? 因为FixedThreadPool和SingleThreadPool使用的是无界队列,会堆积大量的请求,造成OOM。还有CachedThreadPool和ScheduledThreadPool会造成Integer.MAX_VALUE,会创建大量的线程,造成OOM


分析CountDownLatch

CountDownLatch用于协调多个线程的同步,能让一个线程在等待其他线程执行完任务后,再继续执行。内部是通过一个计数器去完成实现。

静态内部类Sync继承AQS,通过state变量完成计数器的实现。

    /*** Synchronization control For CountDownLatch.* Uses AQS state to represent count.*/private static final class Sync extends AbstractQueuedSynchronizer {private static final long serialVersionUID = 4982264981922014374L;Sync(int count) {setState(count);}int getCount() {return getState();}protected int tryAcquireShared(int acquires) {return (getState() == 0) ? 1 : -1;}protected boolean tryReleaseShared(int releases) {// Decrement count; signal when transition to zerofor (;;) {int c = getState();if (c == 0)return false;int nextc = c-1;if (compareAndSetState(c, nextc))return nextc == 0;}}}
复制代码

CountDownLatch构造方法,count代表需要执行的线程数量。

    /*** Constructs a {@code CountDownLatch} initialized with the given count.** @param count the number of times {@link #countDown} must be invoked*        before threads can pass through {@link #await}* @throws IllegalArgumentException if {@code count} is negative*/public CountDownLatch(int count) {if (count < 0) throw new IllegalArgumentException("count < 0");this.sync = new Sync(count);}
复制代码

在计数值 > 0的情况下,每当一个线程完成任务,计数减去1。

    /*** Decrements the count of the latch, releasing all waiting threads if* the count reaches zero.** <p>If the current count is greater than zero then it is decremented.* If the new count is zero then all waiting threads are re-enabled for* thread scheduling purposes.** <p>If the current count equals zero then nothing happens.*/public void countDown() {sync.releaseShared(1);}
复制代码

让当前线程等待其他线程,直到计数为0,除非当前线程被中断了。

    public void await() throws InterruptedException {sync.acquireSharedInterruptibly(1);}
复制代码

让当前线程等待其他线程,如果超过指定的timeout时间范围,那么忽略要等待的线程, 直接执行。

    public boolean await(long timeout, TimeUnit unit)throws InterruptedException {return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));}
复制代码

返回当前计数量。

    /*** Returns the current count.** <p>This method is typically used for debugging and testing purposes.** @return the current count*/public long getCount() {return sync.getCount();}
复制代码

当计数器应该为0,所有的线程执行完自己的任务。在CountDownLatch等待的线程,可以继续执行的任务。


使用场景

适用于一个任务的执行需要等待其他任务执行完毕,方可执行的场景。

场景1:一群学生在教室考试,学生们都完成了作答,老师才可以进行收卷操作。

场景2:110跨栏比赛中,所有运动员准备好起跑姿势,进入到预备状态,等待裁判一声枪响。裁判开了枪,所有运动员才可以开跑。

CountDownLatch是一次性的,只能通过构造方法设置初始计数量,计数完了无法进行复位,不能达到复用。而CyclicBarrier可以实现复用。


场景1

一个线程的执行要等待其他线程执行完毕后,才能继续执行。

    public static void test1() {final CountDownLatch countDownLatch = new CountDownLatch(2);ExecutorService executorService = new CustomThreadPoolExecutor(2,2, 0L,TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(10));for (int i = 0; i < 2; i++) {CustomThreadPoolExecutor.CustomTask task = new CustomThreadPoolExecutor.CustomTask(new Runnable() {@Overridepublic void run() {System.out.println("子线程" + Thread.currentThread().getName()+ "正在执行...");System.out.println("子线程" + Thread.currentThread().getName()+ "执行完毕...");countDownLatch.countDown();}}, "success");executorService.submit(task);}try {System.out.println("等待2个线程...");countDownLatch.await();executorService.shutdown();System.out.println("2个线程执行完毕...");} catch (InterruptedException ex) {ex.printStackTrace();}}
复制代码

输出结果

等待2个线程...
子线程pool-1-thread-2正在执行...
子线程pool-1-thread-2执行完毕...
子线程pool-1-thread-1正在执行...
子线程pool-1-thread-1执行完毕...
2个线程执行完毕..
复制代码

场景2

多个线程在某一个时刻同时执行。

    public static void test2() {final CountDownLatch start = new CountDownLatch(1);final CountDownLatch end = new CountDownLatch(10);ExecutorService executorService = new CustomThreadPoolExecutor(10,10, 0L,TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(10));for (int i = 0; i < 10; i++) {CustomThreadPoolExecutor.CustomTask task = new CustomThreadPoolExecutor.CustomTask(new Runnable() {@Overridepublic void run() {try {System.out.println("子线程" + Thread.currentThread().getName()+ "正在执行...");start.await();System.out.println("子线程" + Thread.currentThread().getName()+ "执行完毕...");} catch (InterruptedException ex) {ex.printStackTrace();} finally {end.countDown();}}}, "success");executorService.submit(task);}start.countDown();try {System.out.println("等待10个线程...");end.await();executorService.shutdown();System.out.println("10个线程执行完毕...");} catch (InterruptedException ex) {ex.printStackTrace();}}复制代码

输出结果

子线程pool-1-thread-2正在执行...
子线程pool-1-thread-1正在执行...
子线程pool-1-thread-3正在执行...
子线程pool-1-thread-4正在执行...
子线程pool-1-thread-5正在执行...
子线程pool-1-thread-6正在执行...
子线程pool-1-thread-7正在执行...
等待10个线程...
子线程pool-1-thread-2执行完毕...
子线程pool-1-thread-8正在执行...
子线程pool-1-thread-8执行完毕...
子线程pool-1-thread-1执行完毕...
子线程pool-1-thread-3执行完毕...
子线程pool-1-thread-4执行完毕...
子线程pool-1-thread-10正在执行...
子线程pool-1-thread-10执行完毕...
子线程pool-1-thread-5执行完毕...
子线程pool-1-thread-6执行完毕...
子线程pool-1-thread-7执行完毕...
子线程pool-1-thread-9正在执行...
子线程pool-1-thread-9执行完毕...
10个线程执行完毕...
复制代码

CountDownLatch源码分析

CountDownLatch的构造方法初始计数器值,是通过其内部类Sync的构造方法来实现的。

        Sync(int count) {setState(count);}
复制代码

AQS中的state变量可以表示状态。对于ReentrantLock而言,代表着锁获取的次数。而对于CountDownLatch代表着计数器的值。state变量通过volatile修饰,具有可见性,可以在多个线程中共享变量。

    /*** Sets the value of synchronization state.* This operation has memory semantics of a {@code volatile} write.* @param newState the new state value*/protected final void setState(int newState) {state = newState;}
复制代码

AQS中的CAS操作,使其state变量具有原子性。

    protected final boolean compareAndSetState(int expect, int update) {// See below for intrinsics setup to support thisreturn unsafe.compareAndSwapInt(this, stateOffset, expect, update);}private static final Unsafe unsafe = Unsafe.getUnsafe();private static final long stateOffset;private static final long headOffset;private static final long tailOffset;private static final long waitStatusOffset;private static final long nextOffset;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); }}
复制代码

我们来看一下AQS中的Node节点结构。

  • 当waitStatus = CANCELLED时,说明因为超时或者中断,节点会被设置为取消状态。处于取消状态的节点不会参与到竞争中。它会一直保持取消状态,会转变到其他状态。
  • 当waitStatus = SIGNAL时,说明当前节点的后继节点处于等待状态。而当前节点的线程如果释放了同步状态或者被取消,将会通知后继节点,使后继节点的线程可以得到运行。
  • 当waitStatus = CONDITION,说明该节点在等待队列中,节点线程等待在Condition上。当其他线程对Condition调用了signal()后,该节点会从等待队列中转移到同步队列中,加入到同步状态的获取中。
  • 当waitStatus = PROPAGATE,说明下一次共享式获取同步状态,将会无条件的传播下去。
static final class Node {/** 共享 */static final Node SHARED = new Node();/** 独占 */static final Node EXCLUSIVE = null;static final int CANCELLED =  1;static final int SIGNAL    = -1;static final int CONDITION = -2;static final int PROPAGATE = -3;/** 等待状态 */volatile int waitStatus;/** 前驱节点 */volatile Node prev;/** 后继节点 */volatile Node next;/** 获取同步状态的线程 */volatile Thread thread;Node nextWaiter;final boolean isShared() {return nextWaiter == SHARED;}final Node predecessor() throws NullPointerException {Node p = prev;if (p == null)throw new NullPointerException();elsereturn p;}Node() {}Node(Thread thread, Node mode) {this.nextWaiter = mode;this.thread = thread;}Node(Thread thread, int waitStatus) {this.waitStatus = waitStatus;this.thread = thread;}
}
复制代码

countDown()实际是调用AQS中的releaseShared方法中,达到计数减1的目的。

    public void countDown() {sync.releaseShared(1);}
复制代码

以共享模式去释放锁,如果tryReleaseShared方法释放锁成功,则执行AQS中的doReleaseShared方法去唤醒等待线程,并且返回true;否则返回false,说明锁释放失败。

    public final boolean releaseShared(int arg) {if (tryReleaseShared(arg)) {doReleaseShared();return true;}return false;}
复制代码

tryReleaseShared在CountDownLatch中被重写。通过轮询 + CAS方式达到释放锁的目的。第一次循环的时候判断当前state变量,如果等于0,说明计数器值为0或者说锁没有被持有,可以直接返回false。然后进行CAS操作,让获取锁的次数减少1或者说计数器值减少1。如果nextc等于0,说明计数值为0或者持有锁的次数为0,可以让唤醒等待的线程,所以返回true,否则返回false,代表释放锁失败。

        protected boolean tryReleaseShared(int releases) {// Decrement count; signal when transition to zerofor (;;) {int c = getState();if (c == 0)return false;int nextc = c-1;if (compareAndSetState(c, nextc))return nextc == 0;}}
复制代码

首先获得head节点。如果head节点不等于空且head节点不等于tail节点,获得head节点的waitStatus。判断当前head节点状态是否是SINGAL。处于SINGAL状态的节点,说明当前节点的后继节点处于被唤醒的状态。如果CAS操作将head节点的waitStatus重置为0失败,那么跳出当前循环,继续执行下一次循环(重新检查)。如果重置成功,那么调用unparkSuccessor方法唤醒后继节点。 如果当前head节点状态等于0,通过CAS操作将waitStatus设置为PROPAGATE(传播)状态,确保可以向后一个节点传播下去。如果CAS操作失败,那么当前循环,继续执行下一次循环。最后的h == head,是判断head节点是否发生变化。如果没有发生变化,结束循环。如果发生变化,必须再次循环。

    /*** Release action for shared mode -- signals successor and ensures* propagation. (Note: For exclusive mode, release just amounts* to calling unparkSuccessor of head if it needs signal.)*/private void doReleaseShared() {/** Ensure that a release propagates, even if there are other* in-progress acquires/releases.  This proceeds in the usual* way of trying to unparkSuccessor of head if it needs* signal. But if it does not, status is set to PROPAGATE to* ensure that upon release, propagation continues.* Additionally, we must loop in case a new node is added* while we are doing this. Also, unlike other uses of* unparkSuccessor, we need to know if CAS to reset status* fails, if so rechecking.*/for (;;) {Node h = head;if (h != null && h != tail) {int ws = h.waitStatus;if (ws == Node.SIGNAL) {if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))continue;            // loop to recheck casesunparkSuccessor(h);}else if (ws == 0 &&!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))continue;                // loop on failed CAS}if (h == head)                   // loop if head changedbreak;}}
复制代码

获取head节点的waitStatus,如果小于0,进行CAS操作重置为0。获取head节点的后继节点,如果后继节点等于null或者后继节点的waitStaus大于0(说明后继节点处于CANCELLED状态),那么从队列从尾部往前进行遍历寻找waitStatus小于等于0的节点。如果这个遍历出来的节点不等于null的话,那么通过LockSupport.unpark()唤醒这个节点中的线程。

    private void unparkSuccessor(Node node) {/** If status is negative (i.e., possibly needing signal) try* to clear in anticipation of signalling.  It is OK if this* fails or if status is changed by waiting thread.*/int ws = node.waitStatus;if (ws < 0)compareAndSetWaitStatus(node, ws, 0);/** Thread to unpark is held in successor, which is normally* just the next node.  But if cancelled or apparently null,* traverse backwards from tail to find the actual* non-cancelled successor.*/Node s = node.next;if (s == null || s.waitStatus > 0) {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);}
复制代码

分析完了CountDownLatch的countDown()方法,接着撸await()方法

await方法会使当前线程在计数器值为0之前,一直处于等待状态,除非中断。

   public void await() throws InterruptedException {sync.acquireSharedInterruptibly(1);}
复制代码

AQS中的acquireSharedInterruptibly方法,会判断线程是否中断。如果中断, 抛出InterruptedException异常。值得注意的是Thread.interrupted()方法,是测试当前线程是否中断。该方法会清除线程的中断状态。换句话说,如果调用这个方法2次,那么第二次会直接返回false,除非当前线程在第一次调用之后再次被中断。如果tryAcquireShared()小于0(说明该计数器值大于0),继续执行doAcquireSharedInterruptibly。

    public final void acquireSharedInterruptibly(int arg)throws InterruptedException {if (Thread.interrupted())throw new InterruptedException();if (tryAcquireShared(arg) < 0)doAcquireSharedInterruptibly(arg);}
复制代码

在CountDownLatch中重写了AQS中的tryAcquireShared()方法,这里只是简单的判断state变量。如果state等于0(说明计数值为0),返回1,否则返回-1(说明计数器值大于0)。

 protected int tryAcquireShared(int acquires) {return (getState() == 0) ? 1 : -1;}复制代码

很明显,是通过轮询的方式去获取共享锁。首先将当前线程包装成类型为SHARED的节点,标志为共享类型的节点。获取当前节点的前驱节点。如果当前节点的前驱节点为head节点的话,说明该节点是在AQS队列中等待获取锁的第一个节点。调用CountDownLatch中的tryAcquireShared()尝试去获取锁。返回的值大于0的话,说明获取锁成功。如果获取共享锁成功,那么把当前节点设置为AQS同步队列中的head节点,同时将p.next置为null(方便GC)。回到头看,如果当前节点的前驱节点不是head节点或者获取锁失败,我们需要调用shouldParkAfterFailedAcquire()方法判断当前线程是否需要挂起,如果需要挂起调用 parkAndCheckInterrupt()

    private void doAcquireSharedInterruptibly(int arg)throws InterruptedException {final Node node = addWaiter(Node.SHARED);boolean failed = true;try {for (;;) {final Node p = node.predecessor();if (p == head) {int r = tryAcquireShared(arg);if (r >= 0) {setHeadAndPropagate(node, r);p.next = null; // help GCfailed = false;return;}}if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())throw new InterruptedException();}} finally {if (failed)cancelAcquire(node);}}
复制代码

通过当前线程构造出Node节点,mode用于标志是独占还是共享。如果队列非空,则快速入队。通过CAS将node节点置为tail节点,并返回node节点。如果CAS失败或者队列为空,那么通过enq()入队。

    private Node addWaiter(Node mode) {Node node = new Node(Thread.currentThread(), mode);// Try the fast path of enq; backup to full enq on failureNode pred = tail;if (pred != null) {node.prev = pred;if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}enq(node);return node;}
复制代码

enq中通过死循环的方式保证节点的正确插入。如果队列为空,那么创建一个新的节点。通过CAS将新节点设置为head节点,同时也将head节点设置为tail节点。当队列只有一个元素时,head节点等于tail节点。在循环中,唯一跳出循环的条件是通过CAS将node节点设置为tail节点。这样的话,enq方法将并发插入节点的请求变得串行化了。

    private Node enq(final Node node) {for (;;) {Node t = tail;if (t == null) { // Must initializeif (compareAndSetHead(new Node()))tail = head;} else {node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;return t;}}}}
复制代码

这里将node节点设置为head节点。经过一些条件判断后,获取head节点的后继节点。如果后继节点等于null或者后继节点也是共享节点,那么调用doReleaseShared去唤醒它。

    private void setHeadAndPropagate(Node node, int propagate) {Node h = head; // Record old head for check belowsetHead(node);/** Try to signal next queued node if:*   Propagation was indicated by caller,*     or was recorded (as h.waitStatus either before*     or after setHead) by a previous operation*     (note: this uses sign-check of waitStatus because*      PROPAGATE status may transition to SIGNAL.)* and*   The next node is waiting in shared mode,*     or we don't know, because it appears null** The conservatism in both of these checks may cause* unnecessary wake-ups, but only when there are multiple* racing acquires/releases, so most need signals now or soon* anyway.*/if (propagate > 0 || h == null || h.waitStatus < 0 ||(h = head) == null || h.waitStatus < 0) {Node s = node.next;if (s == null || s.isShared())doReleaseShared();}}
复制代码

如果pred节点(node节点的前驱节点)的状态是SIGNAL,说明该pred节点的线程如果释放了同步状态或者被取消,会通知其后继节点(也就是node节点)。所以我们可以安全让node节点的线程挂起。如果pred节点处于取消状态,我们进行死循环, 直到pred节点的状态不是取消状态。通过死循环,我们能确保node节点的前驱节点不处于取消状态。反之,如果一开始pred节点不处于取消状态,那么我们通过CAS将pre节点的状态置为SIGNAL,为后面循环涉及到的park操作进行准备。但是有一点要注意,我们进行park之前,要确保当前节点获取锁失败。

   private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {int ws = pred.waitStatus;if (ws == Node.SIGNAL)/** This node has already set status asking a release* to signal it, so it can safely park.*/return true;if (ws > 0) {/** Predecessor was cancelled. Skip over predecessors and* indicate retry.*/do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else {/** waitStatus must be 0 or PROPAGATE.  Indicate that we* need a signal, but don't park yet.  Caller will need to* retry to make sure it cannot acquire before parking.*/compareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;}
复制代码

调用LockSupport.park()挂起当前线程,并且返回中断状态。

    private final boolean parkAndCheckInterrupt() {LockSupport.park(this);return Thread.interrupted();}
复制代码

CountDownLatch的await(long timeout, TimeUnit unit)和await()方法实现方式基本一致,只不过加入了超时机制而已。

   // 返回false,代表超时public boolean await(long timeout, TimeUnit unit)throws InterruptedException {return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));}
复制代码
   // 返回false,代表超时。返回true,代表获得共享锁成功public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)throws InterruptedException {if (Thread.interrupted())throw new InterruptedException();return tryAcquireShared(arg) >= 0 ||doAcquireSharedNanos(arg, nanosTimeout);}
复制代码

如果在nanosTimeout时间范围内,还没有获取共享锁成功的话,直接返回false。spinForTimeoutThreadshold的值为1000nanoseconds。如果shouldParkAfterFailedAcquire(p, node)返回true且超时时间大于阀值spinForTimeoutThreadshold的话,会通过LockSupport.parkNanos(this, nanosTimeout);让线程挂起nanosTimeout时间。这样的策略体现是:如果超时时间很短的话,就不把当前线程挂起,而是通过自旋,这样线程获取锁很快就释放的情况下,可以减少cpu资源和线程挂起和恢复的性能损耗。

    private boolean doAcquireSharedNanos(int arg, long nanosTimeout)throws InterruptedException {if (nanosTimeout <= 0L)return false;final long deadline = System.nanoTime() + nanosTimeout;final Node node = addWaiter(Node.SHARED);boolean failed = true;try {for (;;) {final Node p = node.predecessor();if (p == head) {int r = tryAcquireShared(arg);if (r >= 0) {setHeadAndPropagate(node, r);p.next = null; // help GCfailed = false;return true;}}nanosTimeout = deadline - System.nanoTime();if (nanosTimeout <= 0L)return false;if (shouldParkAfterFailedAcquire(p, node) &&nanosTimeout > spinForTimeoutThreshold)LockSupport.parkNanos(this, nanosTimeout);if (Thread.interrupted())throw new InterruptedException();}} finally {if (failed)cancelAcquire(node);}}
复制代码

总结

通过重写AQS中的模板方法,可以实现共享锁和独占锁。

如果获取共享锁失败,请求共享锁的线程封装成SHARED类型的Node对象并且加入到AQS同步队列中,并挂起Node对象对应的线程,等待锁的释放。待共享锁的可以被获取后,从head节点开始依次唤醒head节点之后的所有SHARED类型的节点,实现共享状态的传播。而独占锁则是,当锁被头节点获取后,只有头节点获得了锁,其他节点的线程继续沉睡。等待锁被释放了,才会唤醒下一个节点的线程,少了setHeadAndPropagate()这一步。

尾言

大家好,我是cmazxiaoma(寓意是沉梦昂志的小马),希望和你们一起成长进步,感谢各位阅读本文章。

如果您对这篇文章有什么意见或者错误需要改进的地方,欢迎与我讨论。 如果您觉得还不错的话,希望你们可以点个赞。 希望我的文章对你能有所帮助。 有什么意见、见解或疑惑,欢迎留言讨论。

最后送上:心之所向,素履以往。生如逆旅,一苇以航。

JUC之CountDownLatch的源码和使用场景分析相关推荐

  1. 【JUC】JDK1.8源码分析之ArrayBlockingQueue(三)

    一.前言 在完成Map下的并发集合后,现在来分析ArrayBlockingQueue,ArrayBlockingQueue可以用作一个阻塞型队列,支持多任务并发操作,有了之前看源码的积累,再看Arra ...

  2. Colly源码解析——结合例子分析底层实现

    通过<Colly源码解析--框架>分析,我们可以知道Colly执行的主要流程.本文将结合http://go-colly.org上的例子分析一些高级设置的底层实现.(转载请指明出于break ...

  3. lodash源码中debounce函数分析

    lodash源码中debounce函数分析 一.使用 在lodash中我们可以使用debounce函数来进行防抖和截流,之前我并未仔细注意过,但是不可思议的是,lodash中的防抖节流函数是一个函数两 ...

  4. Linux内核学习(五):linux kernel源码结构以及makefile分析

    Linux内核学习(五):linux kernel源码结构以及makefile分析 前面我们知道了linux内核镜像的生成.加载以及加载工具uboot. 这里我们来看看linux内核的源码的宏观东西, ...

  5. Java多线程之JUC包:Semaphore源码学习笔记

    若有不正之处请多多谅解,并欢迎批评指正. 请尊重作者劳动成果,转载请标明原文链接: http://www.cnblogs.com/go2sea/p/5625536.html Semaphore是JUC ...

  6. 【JUC】JDK1.8源码分析之ConcurrentHashMap

    一.前言 最近几天忙着做点别的东西,今天终于有时间分析源码了,看源码感觉很爽,并且发现ConcurrentHashMap在JDK1.8版本与之前的版本在并发控制上存在很大的差别,很有必要进行认真的分析 ...

  7. synchronousqueue场景_【JUC】JDK1.8源码分析之SynchronousQueue(九)

    一.前言 本篇是在分析Executors源码时,发现JUC集合框架中的一个重要类没有分析,SynchronousQueue,该类在线程池中的作用是非常明显的,所以很有必要单独拿出来分析一番,这对于之后 ...

  8. JUC锁框架——AQS源码分析

    2019独角兽企业重金招聘Python工程师标准>>> JUC锁介绍 Java的并发框架JUC(java.util.concurrent)中锁是最重要的一个工具.因为锁,才能实现正确 ...

  9. 【JUC】JDK1.8源码分析之ConcurrentLinkedQueue(五)

    一.前言 接着前面的分析,接下来分析ConcurrentLinkedQueue,ConcurerntLinkedQueue一个基于链接节点的无界线程安全队列.此队列按照 FIFO(先进先出)原则对元素 ...

最新文章

  1. 读后感与机翻《从视频中推断力量和学习人类效用》
  2. 安卓java模拟器_用大白话告诉你:Java 后端到底是在做什么?
  3. OSI网络七层模型简明教程
  4. github authentication设置里,fallback SMS number国家选项里没有中国的问题
  5. 从Exchange 通往Office 365系列(十二)发布Outlook Anywhere
  6. Redis-数据结构05-字典(dict)
  7. 青岛工学院计算机专业分数线,青岛工学院分数线
  8. 从用户端到后台系统,严选分销教会我这些事
  9. Adobe DreamweaverCS4 beta+可用序列号,FireworkCS4 beta及SoundboothCS4 beta 官方下载地址...
  10. 安国主控,U盘量产,起死回生
  11. 卫星导航信号结构变化的过去,现在和未来
  12. 1428C ABBB
  13. 2022年镇海夏令营组合数学和数论班 —— 数学作业 1
  14. 三维点云语义分割【综述】 ——Deep Learning for 3D Point Clouds: A Survey
  15. spring boot项目报错:Validation failed for query for method public abstract...
  16. ERD Online 4.0.3_fix 元数据在线建模(免费、私有部署)
  17. 华为鸿蒙开发者公测报名,华为发布鸿蒙2.0手机开发者测试版!华为老手机可申请公测...
  18. Password Management: Hardcoded Password
  19. numpy实现torch的topk方法
  20. linux日志切割命令,Linux 服务器log日志切割三种方法【附命令行】

热门文章

  1. Swift中@IBDesignable/@IBInspectable的使用
  2. (0077)iOS开发之直播播放器技术名词理解以及开发准备(待实现直播demo)
  3. es搜索引擎架构_学弟想学搜索引擎,我把珍藏的京东架构师的ES笔记分享给了他...
  4. 字符串与base64相互转换
  5. 【bzoj1444】[Jsoi2009]有趣的游戏 AC自动机+矩阵乘法
  6. Spring ContextLoaderListener与DispatcherServlet所加载的applicationContext的区别
  7. 怎样用HTML5 Canvas制作一个简单的游戏
  8. c# webrequest 自动登入网站
  9. 浅谈Python Web的五大框架
  10. 【解决VMWare中新建虚拟机不支持centos64位的方法】