JUC源码分析-线程池篇(一):ThreadPoolExecutor

Java 中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理地使用线程池能够带来 3 个好处。

  • 第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 第三:提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用线程池,必须对其实现原理了如指掌。

1. ThreadPoolExecutor 执行流程

ThreadPoolExecutor 执行 execute 方法分下面 4 种情况。

1)如果当前运行的线程少于 corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)。

2)如果运行的线程等于或多于 corePoolSize,则将任务加入 BlockingQueue。

3)如果无法将任务加入 BlockingQueue(队列已满),则创建新的线程来处理任务(注意,执行这一步骤需要获取全局锁)。

4)如果创建新线程将使当前运行的线程超出 maximumPoolSize,任务将被拒绝,并调用 RejectedExecutionHandler.rejectedExecution() 方法。

ThreadPoolExecutor 采取上述步骤的总体设计思路,是为了在执行 execute() 方法时,尽可能地避免获取全局锁(那将会是一个严重的可伸缩瓶颈)。在 ThreadPoolExecutor 完成预热之后(当前运行的线程数大于等于 corePoolSize),几乎所有的 execute() 方法调用都是执行步骤2,而步骤2不需要获取全局锁。

上面的流程分析让我们很直观地了解了线程池的工作原理,让我们再通过源代码来看看是如何实现的,线程池执行任务的方法如下。我们从 execute 入手分析源码。

2. ThreadPoolExecutor 源码分析

2.1 主要属性

2.1.1 线程池生命周期

  1. private static final int RUNNING = -1 << COUNT_BITS; // 运行状态
  2. private static final int SHUTDOWN = 0 << COUNT_BITS; // 拒绝提交新任务,会执行完已提交的任务后关闭线程池
  3. private static final int STOP = 1 << COUNT_BITS; // 拒绝提交新任务,也拒绝执行已提交的任务
  4. private static final int TIDYING = 2 << COUNT_BITS; // 关闭线程池后,线程全部关闭后的状态,之后回调 terminated
  5. private static final int TERMINATED = 3 << COUNT_BITS; // 回调 terminated 方法后状态变为 TERMINATED

线程池用 ctl 的低 29 位表示线程池中的线程数,高 3 位表示当前线程状态。

  • RUNNING:运行状态,高3位为111;
  • SHUTDOWN:关闭状态,高3位为000,在此状态下,线程池不再接受新任务,但是仍然处理阻塞队列中的任务;
  • STOP:停止状态,高3位为001,在此状态下,线程池不再接受新任务,也不会处理阻塞队列中的任务,正在运行的任务也会停止;
  • TIDYING:高3位为010;
  • TERMINATED:终止状态,高3位为011。

2.1.2 线程状态标识 ctl

  1. // ctl 高3位表示线程池状态,低29位表示当前工作线程数
  2. private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
  3. private static final int COUNT_BITS = Integer.SIZE - 3; // 低29位表示工作线程数
  4. private static final int CAPACITY = (1 << COUNT_BITS) - 1; // 最大线程数 0x1fffffff
  5. // 获取线程池状态、线程总数、构造 ctl
  6. private static int runStateOf(int c) { return c & ~CAPACITY; }
  7. private static int workerCountOf(int c) { return c & CAPACITY; }
  8. private static int ctlOf(int rs, int wc) { return rs | wc; }

2.1.3 其它属性

  1. // 全局锁,创建工作线程等操作时需要获取全局锁
  2. private final ReentrantLock mainLock = new ReentrantLock();
  3. private final Condition termination = mainLock.newCondition();
  4. // 工作线程
  5. private final HashSet<Worker> workers = new HashSet<Worker>();
  6. private int largestPoolSize;
  7. private volatile int corePoolSize;

2.2 任务提交 execute

  1. public void execute(Runnable command) {
  2. if (command == null)
  3. throw new NullPointerException();
  4. // ctl 高3位表示线程池状态,低29位表示当前工作线程数
  5. int c = ctl.get();
  6. // 1. 小于核心线程数,创建新的线程执行任务。需要获取全局锁
  7. if (workerCountOf(c) < corePoolSize) {
  8. // addWorker 创建新的工作线程,true 表示核心线程数,false 表示最大线程数
  9. if (addWorker(command, true))
  10. return;
  11. c = ctl.get();
  12. }
  13. // 2. 核心线程已满,将任务提交到队列中。不需要获取全局锁
  14. if (isRunning(c) && workQueue.offer(command)) {
  15. int recheck = ctl.get();
  16. // 2.1 刚好此时线程池关闭了,则需要将任务从队列中踢除
  17. if (!isRunning(recheck) && remove(command))
  18. reject(command); // 任务被踢除后回滚,执行拒绝任务
  19. // 2.2 线程池工作线程为0,创建一个新的工作线程
  20. else if (workerCountOf(recheck) == 0)
  21. addWorker(null, false);
  22. }
  23. // 3. 队列满后且线程数小于最大线程数,则创建新的线程执行任务。需要获取全局锁
  24. // 4. 超出最大线程拒绝任务
  25. else if (!addWorker(command, false))
  26. reject(command);
  27. }

2.3 工作线程 Worker

工作线程:线程池创建线程时,会将线程封装成工作线程 Worker,Worker 在执行完任务后,还会循环获取工作队列里的任务来执行。我们可以从 Worker 类的 run() 方法里看到这点。

  1. // Worker 是对线程 Thread 的包装,实现了 AbstractQueuedSynchronizer
  2. private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
  3. final Thread thread; // 包装的线程
  4. Runnable firstTask; // 线程初始化时的任务,可以为 null
  5. Worker(Runnable firstTask) {
  6. setState(-1); // inhibit interrupts until runWorker
  7. this.firstTask = firstTask;
  8. this.thread = getThreadFactory().newThread(this);
  9. }
  10. public void run() {
  11. runWorker(this);
  12. }
  13. }

思考:Worker 为什么要继承 AbstractQueuedSynchronizer 实现自己的锁,而不使用 ReentrantLock 呢?

实际上 ReentrantLock 是可重入锁,而 Worker 实现的是独占锁,只有三种状 -1(初始化)、0(释放锁)、1(占有锁)。Worker 之所以实现独占锁是为了避免在线程执行的时候被 interrupted 中断(下面会讲到)。

2.4 创建工作线程 addWorker

  1. // addWorker 创建一个新的工作线程
  2. // firstTask 线程初始化任务,可以为 null;core 表示是核心线程还是最大线程
  3. private boolean addWorker(Runnable firstTask, boolean core) {
  4. // 1. 通过自旋线程数+1 compareAndIncrementWorkerCount
  5. retry:
  6. for (;;) {
  7. int c = ctl.get();
  8. int rs = runStateOf(c);
  9. // 1.1 一、STOP 不能创建新线程
  10. // 二、SHUTDOWN 时 workQueue 为空,也不能创建新线程
  11. // firstTask 表示线程初始化任务,是新提交的任务,SHUTDOWN 时拒绝新提交的任务
  12. if (rs >= SHUTDOWN &&
  13. ! (rs == SHUTDOWN && firstTask == null && !workQueue.isEmpty()))
  14. return false;
  15. // 1.2 自旋使线程数+1
  16. for (;;) {
  17. int wc = workerCountOf(c);
  18. if (wc >= CAPACITY ||
  19. wc >= (core ? corePoolSize : maximumPoolSize))
  20. return false;
  21. if (compareAndIncrementWorkerCount(c))
  22. break retry;
  23. c = ctl.get(); // Re-read ctl
  24. if (runStateOf(c) != rs) // 不断检查线程池状态变化
  25. continue retry;
  26. // else CAS failed due to workerCount change; retry inner loop
  27. }
  28. }
  29. // 2. 创建线程 Worker
  30. boolean workerStarted = false;
  31. boolean workerAdded = false;
  32. Worker w = null;
  33. try {
  34. // 2.1 初始化工作线程 Worker,使用全局锁添加到 workers 队列中
  35. w = new Worker(firstTask);
  36. final Thread t = w.thread; // threadFactory 可能创建线程失败,返回 null
  37. if (t != null) {
  38. final ReentrantLock mainLock = this.mainLock;
  39. mainLock.lock();
  40. try {
  41. int rs = runStateOf(ctl.get());
  42. // 2.2 一、RUNNING可以创建新线程
  43. // 二、SHUTDOWN不接收新任务,但会执行完 workQueue 的任务 ,因此可以创建空任务的线程
  44. if (rs < SHUTDOWN ||
  45. (rs == SHUTDOWN && firstTask == null)) {
  46. if (t.isAlive()) // precheck that t is startable
  47. throw new IllegalThreadStateException();
  48. workers.add(w);
  49. int s = workers.size();
  50. if (s > largestPoolSize) // largestPoolSize 表示线程池运行过程中达到的最大线程数
  51. largestPoolSize = s;
  52. workerAdded = true; // 工作线程添加到 workers 成功
  53. }
  54. } finally {
  55. mainLock.unlock();
  56. }
  57. if (workerAdded) {
  58. t.start();
  59. workerStarted = true; // 启动线程成功
  60. }
  61. }
  62. } finally {
  63. // 2.3 创建工作线程失败,回滚
  64. if (! workerStarted)
  65. addWorkerFailed(w);
  66. }
  67. return workerStarted;
  68. }

总结:

  1. addWorker 前半部分主要是判断能否新建工作线程,如果允许则执行 compareAndIncrementWorkerCount(c),利用 CAS 原则,将线程数量+1。
  2. addWorker 后半部分则是真正创建工作线程并启动,这个过程需要获取全局锁。创建失败则需要回滚 addWorkerFailed。

addWorker 的 4 种调用方式:

  1. addWorker(command, true) 线程数 < coreSize 时,则创建新线程
  2. addWorker(command, false) 当①阻塞队列已满,②线程数 < maximumPoolSize 时,则创建新线程
  3. addWorker(null, true) 同 1。只是线程初始化任务为 null,相当于创建一个新的线程。实际的使用是在 prestartCoreThread() 等方法,有兴趣的读者可以自行阅读,在此不做详细赘述。
  4. addWorker(null, false) 同 2。只是线程初始化任务为 null,相当于创建一个新的线程,没立马分配任务;

2.4 线程执行 runWorker

在 addWorker 创建线程后调用 t.start() 启动线程,run 方法主要干了一件事,调用 runWorker(this),接下来我们来看看 runWorker 的具体实现。

  1. final void runWorker(Worker w) {
  2. Thread wt = Thread.currentThread();
  3. Runnable task = w.firstTask; // 线程初始化任务 task
  4. w.firstTask = null;
  5. // 1. Worker 是独占锁,此时状态由 -1 -> 0,也就是其它线程才能获取w的锁,进而interrupt
  6. w.unlock(); // allow interrupts
  7. boolean completedAbruptly = true;
  8. try {
  9. // 2. 循环通过 getTask 获取任务,如果不能获取任务了,退出循环,关闭线程池
  10. // 也就是说 getTask 返回 null 时线程就关闭了
  11. while (task != null || (task = getTask()) != null) {
  12. w.lock(); // 获取锁,这样在线程执行过程中不能中断线程(interrupt)
  13. // If pool is stopping, ensure thread is interrupted;
  14. // if not, ensure thread is not interrupted. This
  15. // requires a recheck in second case to deal with
  16. // shutdownNow race while clearing interrupt
  17. //
  18. // 3.1 线程池已经STOP,如果线程还没有被中断(wt.isInterrupted=false),则调用wt.interrupt中断线程
  19. // 3.2 如果runStateAtLeast(ctl.get(), STOP)=false,则说明线程池处于RUNNING或SHUTDOWN状态
  20. // 调用 Thread.interrupted() 后会清空线程的 interrupted 状态
  21. // Thread.interrupted()&& false 结果始终为 false,这里仅仅是为了调用Thread.interrupted()
  22. // 实际上就是:一如果线程已经STOP,则一定要将线程 interrupt
  23. // 二如果线程处于运行状态(包括SHUTDOWN),则一定不能 interrupt(也就是要清除 interrupt 标记)
  24. if ((runStateAtLeast(ctl.get(), STOP) ||
  25. (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP)))
  26. && !wt.isInterrupted())
  27. wt.interrupt();
  28. try {
  29. beforeExecute(wt, task); // 执行前
  30. Throwable thrown = null;
  31. try {
  32. task.run(); // 执行任务
  33. } catch (RuntimeException x) {
  34. thrown = x; throw x;
  35. } catch (Error x) {
  36. thrown = x; throw x;
  37. } catch (Throwable x) {
  38. thrown = x; throw new Error(x);
  39. } finally {
  40. afterExecute(task, thrown); // 执行后
  41. }
  42. } finally {
  43. task = null;
  44. w.completedTasks++; // 统计执行的任务数
  45. w.unlock(); // 释放锁,可以被中断了
  46. }
  47. }
  48. completedAbruptly = false; // true时表示正常退出,false表示异常退出
  49. } finally {
  50. processWorkerExit(w, completedAbruptly);
  51. }
  52. }

总结,runWoker 具体实现:

  1. 线程启动后,释放锁,设 AQS 状态为 0,释放锁。此时其它线程才可以获取锁,中断线程 interrupt;
  2. 获取 firstTask 任务并执行,执行任务前后可定制 beforeExecute 和 afterExecute;
  3. 如果 getTask 从阻塞队列获取等待任务执行,如果获取的任务为 null,while 则退出循环,线程关闭。
  4. 如果线程已经STOP,则一定要将线程 interrupt。如果线程处于运行状态(包括SHUTDOWN),则一定不能 interrupt。但实际上 interrupt() 方法并不一定能中断正在运行的线程,它只能唤醒 wait 阻塞的线程或给线程设置一个标记位。业务线程必须对 interrupt 做出响应才能中断线程,否则会一直等线程执行结束才会销毁。

2.5 获取任务 getTask

  1. // 注意 getTask 前 worker 释放了锁,也就是可能被 interrupt 唤醒
  2. private Runnable getTask() {
  3. boolean timedOut = false; // Did the last poll() time out?
  4. for (;;) { // 自旋获取任务
  5. int c = ctl.get();
  6. int rs = runStateOf(c);
  7. // 1. ①STOP直接销毁线程,②SHUTDOWN时任务队列为空时也直接销毁线程
  8. if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
  9. decrementWorkerCount(); // 原子性更新,工作线程数-1
  10. return null;
  11. }
  12. int wc = workerCountOf(c); // 当前工作线程数
  13. // 2.1 timed表示是否可以销毁线程。timed=true表示超时获取任务,则可能返回null
  14. // 当线程数大于核心线程数或允许销毁核心线程时 timed=true
  15. boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
  16. // 2.2 一是超过了最大线程数,当线程池启动后手动修改最大线程数可能会出现这种情况
  17. // 二是当允许销毁线程时,获取任务超时
  18. // 2.3 三是线程池中至少有一个工作线程或任务队列为空,则可以销毁线程
  19. if ((wc > maximumPoolSize || (timed && timedOut))
  20. && (wc > 1 || workQueue.isEmpty())) {
  21. if (compareAndDecrementWorkerCount(c)) // 失败重试,此时线程数已经-1
  22. return null;
  23. continue;
  24. }
  25. try {
  26. // 3. 获取任务,无限等待则不会返回 null,也就不会销毁线程。而限时等待则可能返回 null
  27. Runnable r = timed ?
  28. workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
  29. workQueue.take();
  30. if (r != null)
  31. return r;
  32. timedOut = true;
  33. } catch (InterruptedException retry) {
  34. timedOut = false; // 其它线程唤醒等待的线程
  35. }
  36. }
  37. }

总结,整个 getTask 循环实现:

  1. getTask 时,worker 已经释放了锁,也就是说其它线程可以调用 wt.interrupt() 唤醒等待的线程。
  2. 如果当前线程数大于最大线程数,或允许核心线程销毁时,如果获取任务超时则返回 null,即销毁线程。

2.6 线程关闭

2.6.1 shutdown 和 shutdownNow

  1. public void shutdown() {
  2. final ReentrantLock mainLock = this.mainLock;
  3. mainLock.lock();
  4. try {
  5. checkShutdownAccess(); // 权限检查
  6. advanceRunState(SHUTDOWN); // 更新线程池状态为 SHUTDOWN
  7. interruptIdleWorkers(); // 关闭所有的空闲线程
  8. onShutdown(); // 子类实现,如 ScheduledThreadPoolExecutor
  9. } finally {
  10. mainLock.unlock();
  11. }
  12. tryTerminate(); // 尝试停止线程池
  13. }
  14. public List<Runnable> shutdownNow() {
  15. List<Runnable> tasks;
  16. final ReentrantLock mainLock = this.mainLock;
  17. mainLock.lock();
  18. try {
  19. checkShutdownAccess(); // 权限检查
  20. advanceRunState(STOP); // 更新线程池状态为 SHUTDOWN
  21. interruptWorkers(); // 关闭所有的线程
  22. tasks = drainQueue(); // 返回还未执行的任务
  23. } finally {
  24. mainLock.unlock();
  25. }
  26. tryTerminate(); // 尝试停止线程池
  27. return tasks;
  28. }

总结,shutdown 和 shutdownNow 区别:

  1. shutdown 会执行完成已提交的任务后关闭线程池,而 shutdownNow 则会踢除已提交的任务。
  2. shutdown 调用 interruptIdleWorkers 关闭空闲的线程,而 shutdownNow 调用 interruptWorkers 强行中断所有的线程。

2.6.2 interruptIdleWorkers 和 interruptWorkers

  1. // 关闭所有的空闲线程
  2. private void interruptIdleWorkers() {
  3. interruptIdleWorkers(false);
  4. }
  5. // 中断线程实际上是调用 t.interrupt(),需要获取线程锁 w.tryLock
  6. private void interruptIdleWorkers(boolean onlyOne) {
  7. final ReentrantLock mainLock = this.mainLock;
  8. mainLock.lock();
  9. try {
  10. for (Worker w : workers) {
  11. Thread t = w.thread;
  12. // 只有空闲线程才能获取锁,正在执行的线程无法获取锁,也就无法中断
  13. // 这也就是为什么 Worker 要实现独占锁的原因。
  14. if (!t.isInterrupted() && w.tryLock()) { // 需要获取w的独占锁
  15. try {
  16. t.interrupt(); // 实际上是调用 t.interrupt() 中断线程
  17. // 实际上是给能线程设置一个标记位
  18. } catch (SecurityException ignore) {
  19. } finally {
  20. w.unlock();
  21. }
  22. }
  23. if (onlyOne)
  24. break;
  25. }
  26. } finally {
  27. mainLock.unlock();
  28. }
  29. }

interruptIdleWorkers 只会尝试获取锁,因此只会中断空闲线程。而 interruptWorkers 不需要获取锁,强行中断线程。实际上业务线程必须对 interrupt 做出响应才能中断线程,否则会一直等线程执行结束才会销毁。

  1. private void interruptWorkers() {
  2. final ReentrantLock mainLock = this.mainLock;
  3. mainLock.lock();
  4. try {
  5. for (Worker w : workers)
  6. w.interruptIfStarted();
  7. } finally {
  8. mainLock.unlock();
  9. }
  10. }
  11. // 调用Worker#interruptIfStarted 不需要获取锁
  12. void interruptIfStarted() {
  13. Thread t;
  14. if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
  15. try {
  16. t.interrupt();
  17. } catch (SecurityException ignore) {
  18. }
  19. }
  20. }

而 interruptIdleWorkers 和 interruptWorkers 都是 interrupt 所有线程, 因此大部分线程将立刻被中断。之所以是大部分,而不是全部,是因为 interrupt() 方法能力有限。 如果线程中没有 sleep 、wait、Condition、定时锁等应用, interrupt() 方法是无法中断当前的线程的。所以,ShutdownNow() 并不代表线程池就一定立即就能退出,它可能必须要等待所有正在执行的任务都执行完成了才能退出。 如下面这个线程永远不会中断,因为该线程没有响应 Thread.interrupted() 或者是直接将 InterruptedException 异常 catch 了。

  1. // 无法响应 interrupted,线程永远无法中止。
  2. executorService.submit(() -> { while (true) System.out.println("go go go"); });
  3. executorService.shutdownNow();

2.6.3 tryTerminate

  1. final void tryTerminate() {
  2. for (;;) {
  3. int c = ctl.get();
  4. // 1. RUNNING或SHUTDOWN还有任务执行时不能关闭,TIDYING则已经关闭
  5. if (isRunning(c) || // 1.1 正在运行,不能中断
  6. runStateAtLeast(c, TIDYING) || // 1.2 已经中断,不需要执行
  7. (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty())) // 1.3 SHUTDOWN时还有任务执行
  8. return;
  9. // 2. 还有线程则关闭空闲线程
  10. if (workerCountOf(c) != 0) { // Eligible to terminate
  11. interruptIdleWorkers(ONLY_ONE);
  12. return;
  13. }
  14. final ReentrantLock mainLock = this.mainLock;
  15. mainLock.lock();
  16. try {
  17. // 3. 工作线程数为0时,可以关闭线程池了,设置线程状态为TIDYING,
  18. // 并回调terminated后,线程的状态最终变为TERMINATED
  19. // 4. 线程状态设置失败,则 CAS 自旋
  20. if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
  21. try {
  22. terminated();
  23. } finally {
  24. ctl.set(ctlOf(TERMINATED, 0));
  25. termination.signalAll();
  26. }
  27. return;
  28. }
  29. } finally {
  30. mainLock.unlock();
  31. }
  32. // else retry on failed CAS
  33. }
  34. }

除了 shutdown 和 shutdownNow 外,addWorkerFailed、processWorkerExit、remove 等方法也会调用 tryTerminate 方法。

参考:

  1. 《Java并发编程的艺术》

每天用心记录一点点。内容也许不重要,但习惯很重要!

JUC源码分析-线程池篇(一):ThreadPoolExecutor的更多相关文章

  1. JUC源码分析-线程池篇(三)ScheduledThreadPoolExecutor

    JUC源码分析-线程池篇(三)ScheduledThreadPoolExecutor ScheduledThreadPoolExecutor 继承自 ThreadPoolExecutor.它主要用来在 ...

  2. JUC源码分析-线程池篇(二)FutureTask

    JUC源码分析-线程池篇(二)FutureTask JDK5 之后提供了 Callable 和 Future 接口,通过它们就可以在任务执行完毕之后得到任务的执行结果.本文从源代码角度分析下具体的实现 ...

  3. JUC源码分析-线程池篇(三)Timer

    JUC源码分析-线程池篇(三)Timer Timer 是 java.util 包提供的一个定时任务调度器,在主线程之外起一个单独的线程执行指定的计划任务,可以指定执行一次或者反复执行多次. 1. Ti ...

  4. Elasticsearch源码分析—线程池(十一) ——就是从队列里处理请求

    Elasticsearch源码分析—线程池(十一) 转自:https://www.felayman.com/articles/2017/11/10/1510291570687.html 线程池 每个节 ...

  5. nginx源码分析线程池详解

    nginx源码分析线程池详解 一.前言     nginx是采用多进程模型,master和worker之间主要通过pipe管道的方式进行通信,多进程的优势就在于各个进程互不影响.但是经常会有人问道,n ...

  6. 鸿蒙内核源码分析(线程概念篇) | 是谁在不停的折腾CPU? | 百篇博客分析OpenHarmony源码 | v21.06

    百篇博客系列篇.本篇为: v21.xx 鸿蒙内核源码分析(线程概念篇) | 是谁在不断的折腾CPU | 51.c.h .o 任务管理相关篇为: v03.xx 鸿蒙内核源码分析(时钟任务篇) | 触发调 ...

  7. nginx源码分析——线程池

    源码: nginx 1.13.0-release   一.前言      nginx是采用多进程模型,master和worker之间主要通过pipe管道的方式进行通信,多进程的优势就在于各个进程互不影 ...

  8. 鸿蒙内核源码分析(任务切换篇) | 看汇编如何切换任务 | 百篇博客分析OpenHarmony源码 | v41.03

    百篇博客系列篇.本篇为: v41.xx 鸿蒙内核源码分析(任务切换篇) | 看汇编如何切换任务 | 51.c.h .o 任务管理相关篇为: v03.xx 鸿蒙内核源码分析(时钟任务篇) | 触发调度谁 ...

  9. 鸿蒙内核源码分析(并发并行篇) | 听过无数遍的两个概念 | 百篇博客分析OpenHarmony源码 | v25.01

    百篇博客系列篇.本篇为: v25.xx 鸿蒙内核源码分析(并发并行篇) | 听过无数遍的两个概念 | 51.c.h .o 任务管理相关篇为: v03.xx 鸿蒙内核源码分析(时钟任务篇) | 触发调度 ...

随机推荐

  1. javaScript的事件冒泡事件捕获

    (1)冒泡型事件:事件按照从最特定的事件目标到最不特定的事件目标(document对象)的顺序触发. IE 5.5: div -> body -> document IE 6.0: div ...

  2. LOJ #103. 子串查找 (Hash)

    题意 给定两个字符串 \(A\) 和 \(B\),求 \(B\) 在 \(A\) 中的出现次数. 思路 这是一道 \(KMP\) 的模板题. 不过 \(Hash\) 是个好东西,可以用 \(Hash\ ...

  3. os.fork()----linux

    fork() 函数,它也属于一个内建并 且只在 Linux 系统下存在. 它非常特殊普通的函数调用,一次返 回但是 fork() 调用一次,返回两次.因为操作系统自动把当前进程(称为父)复制了一份(称 ...

  4. Java-技术专区-虚拟机系列-内存模型(JMM)

           Java8内存模型—永久代(PermGen)和元空间(Metaspace) 一.JVM 内存模型 根据 JVM 规范,JVM 内存共分为虚拟机栈.堆.方法区.程序计数器.本地方法栈五个部 ...

  5. MyBatis笔记一:GettingStart

    MyBatis笔记一:GettingStart 1.MyBatis优点 我们的工具和各种框架的作用就是为了我们操作数据库简洁,对于一些数据库的工具能帮我们少写一些处理异常等等的代码,但是他们并不是自动 ...

  6. 与JS报错的那段时光

    1.Uncaught SyntaxError: Unexpected end of input js报错: 翻译:语法错误:输入意外终止 原因:页面代码写的不规范  ╮(╯▽╰)╭ 其中的某条语句,没 ...

  7. Spring中AOP的基于xml开发和配置

    pom文件: <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http ...

  8. cmd登录mysql、查所有的库、查所有的表、查表下的所有字段

    一.设置好mysql的环境变量,cmd之后输入mysql -u root  -p 输入password进入mysql 二.展示所有的库名show  batabases: 三.选择一个库名use dem ...

  9. 基于Libpcap实现一个网络数据包嗅探器

    基本功能就是来捕获所有流经本网卡的数据包. 实现流程: 查找网络设备 打开网络设备 查找设备信息 输入过滤规则 编译输入规则 设置输入规则 开始捕获数据包 调用数据包分析模块 输出MAC,IP,协议以 ...

  10. 解决vue项目打包之后出现源代码的问题

    config/index.js 页面找到productionSourceMap:ture 改为 productionSourceMap:false