易燃物汇聚 YouTube 有声书与书评
Java高并发与集合框架:JCF和JUC源码分析与实现 封面

Java高并发与集合框架:JCF和JUC源码分析与实现

银文杰

786人在读
21.9万字总字数
¥83.3定价

内容简介

本书主要对Java集合框架(JCF)和Java并发工具包(JUC)进行介绍,包括它们的适用场景、使用方法、技术理论和运行原理。为了让读者能够轻松阅读本书,本书中所有内容都采用由浅入深的方式进行介绍,先保证读者会用这些技术,再介绍这些技术的运行原理。本书分为3部分,第1部分为Java编程入门知识,方便初学者对JCF相关知识进行查漏补缺,第2 部分和第3部分对基础知识有一定的要求,适合有一些Java编程基础的技术人员阅读。

读者书评

★★★★★

这本书作为为数不多讲并发和集合的书,对我帮助很大。笔记如下: 进程与线程之间的区别?进程是资源分配最小单位,线程是程序执行的最小单位 进程有自己独立的地址空间,每启动一个进程,系统都会为其分配地址空间,建立数据表来维护代码段、堆栈段和数据段,线程没有独立的地址空间,它使用相同的地址空间共享数据 CPU切换一个线程比切换进程花费小 创建一个线程比进程开销小 线程占用的资源要⽐进程少很多 线程之间通信更方便,同一个进程下,线程共享全局变量,静态变量等数据,进程之间的通信需要以通信的方式(IPC)进行;(但多线程程序处理好同步与互斥是个难点) 多进程程序更安全,生命力更强,一个进程死掉不会对另一个进程造成影响(源于有独立的地址空间),多线程程序更不易维护,一个线程死掉,整个进程就死掉了(因为共享地址空间) 进程对资源保护要求高,开销大,效率相对较低,线程资源保护要求不高,但开销小,效率高,可频繁切换 线程在进程下行进(单纯的车厢无法运行) 一个进程可以包含多个线程(一辆火车可以有多个车厢) 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘) 同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易) 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源) 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢) 进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上) 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-”互斥锁” 进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量” 什么是线程池?为什么要使用线程池?1. 从科技技术发展的角度来看,摩尔定律受制于现有工艺水平遇到了无法突破的瓶颈,多核 CPU 并行计算提升服务器的性能成为了主流,随之出现了多线程艺术2. 从业务开发的角度来看,假如一个方法的调用链过长,会导致程序的运行时间过长,进而导致系统的吞吐量过长,所以我们采用多线程对方法的调用链进行优化,对于调用关系耦合度不是太高采用多线程走异步的方式进行优化,这样可以降低程序的执行的时间,对于多线程我们创建线程是采用的 new Thread() 的方式去创建对象,但是这样的方式是有弊端的 1. 弊端1 会有 OOM 内存溢出问题:比如当前方法突然遇上高并发请求,当前方法遇上 5000 个请求,如果当前网络 IO 模型是 BIO 模型的话,BIO 是同步阻塞模式,就是服务器实现模式是一个连接一个线程,在不考虑 web 容器开启最大线程数的配置下,这个时候服务器会开启 5000 个线程去处理请求,执行请求的 5000 个线程去调用执行方法发现里面有 new Thead() ,那么这样会创建 5000 个线程,这个时候总共为 5000 * 2 = 10000 条线程,而创建线程是需要向 JVM 申请内存的,所以大量线程瞬间向 JVM 申请内存,会造成 OOM 内存溢出问题 2. 弊端二 会有资源开销和耗时问题:一个对象的生命周期分为对象的创建、对象的使用、对象的回收,真正有效的时间是对象的使用,同理线程作为类的实例,真正有效的时间是线程的工作时间,而线程的创建和销毁是非常消耗时间。如果线程数量非常多的情况下,可能会导致线程创建和销毁的时机远大于线程执行的时间,这个开销有点亏,另外 JVM 垃圾回收这些线程,会导致回收的停顿时间延长,最后,线程的创建相对于普通的对象要更为复杂,一是除了初始化堆空间,还多了一个至少分配 1MB 的栈空间,而是创建线程需要在操作系统内核注册本地线程,这个过程会涉及系统调用,而系统调用是有开销非常大的 3. 弊端三 会有线程不可管理性,因为创建线程后是不可管理的,一旦 CPU 调度成功,这个线程的可管理性几乎为零3. 所以,诞生了线程池去解决这些弊端,线程池采用池化的思想,一旦有任务,无需去创建一个线程去执行,而是线程池里面已经存在的线程去执行任务,执行完任务之后,直接退回到线程池即可,不需要回收,所以线程池的好处很明显 1. 一是提高任务的执行速度,换句话说提高系统的响应速度,任务直接交给已经创建的线程去执行 2. 而是减少资源开销,因为不需要频繁地创建线程和销毁线程了,也没有大量的 CPU 上下文切换开销 3. 三是提高线程的管理性,线程池可以对线程进行管理(调整核心线程数、最大线程数)、监控、调优等 4. 四是降低使用的复杂度,将任务的提交和执行解耦,任务直接提交给线程池即可。具体的执行流程由线程池来做,用户无需关心4. 一个完整的线程池,应该包含了以下几个核心部分 1. 任务提交:提供接口接受任务的提交 2. 任务管理: 选择合适的队列对提交的任务进行管理,包括对拒绝策略的设置 3. 任务执行:由工作线程来执行提交的任务 4. 线程池管理:包括基本参数设置、任务监控、工作线程管理5. 顶级接口 Executor 只定义了 execute(Runnable command)方法解耦的任务的提交和执行,至于具体任务怎么执行则交给他的实现者去自定义实现6. ExecutorService 接口继承 Executor,在这个基础上提供可以管理服务和执行结果的能力,ExecutorService 所提供的 submit 方法可以返回任务的执行结果,而 shutdown 可以用于关闭服务7. AbstractExecutorService 抽象类继承 ExecutorService 接口,对 ExecutorService 相关方法提供了默认实现,用 RunnableFuture 的实现类 FutureTask 包装 Runnable 任务,交给 execute() 方法去执行,然后可以从该 FutureTask 阻塞获取执行结果,并且对批量任务的提交做了编排8. ThreadPoolExecutor 是线程池的最终实现类,定义了任务管理策略和线程池管理能力。 线程池有哪些核心参数 都有哪些核心参数?包括核心线程数(corePoolSize)、最大线程数(maximumPoolSize)、空闲线程超时时间(keepAliveTime)、时间单位(unit)、阻塞队列(workerQueue)、拒绝策略(handler)、线程工厂(ThreadFactory) 当用户线程提交任务时线程池的执行流程如下: 如果任务为空,则抛出 NullPointerException 获取当前线程池的状态 + 线程个数变量的组合值,前 3 位表示状态,后 29 位表示线程池个数 如果当前线程个数小于核心线程数,就新建一个线程来处理提交的任务 如果当前线程个数大于核心线程数,判断线程池是否处于 Running 状态,是则将任务添加到则阻塞队列等待执行,这里需要判断线程池状态是因为可能线程池已经处于非 Running 状态,而在非 Running 状态下是要抛弃新任务的 如果添加任务成功,则再次获取当前线程池的状态 + 线程个数变量的组合值,这是因为添加任务到线程池之后,线程池的状态可能发生变化了 所以当前线程状态不是 RUNNING 则把任务从阻塞队列移除,并执行拒绝策略 如果二次校验通过,则重新判断当前线程池是否为空,为空则添加一个线程 如果执行到这个 if 说明添加任务失败,队列已满,则尝试开启新线程来执行任务,如果当前线程数大于最大线程数则执行拒绝策略 以上这个执行流程是 JUC 标准线程池提供的执行流程,主要用在 CPU 密集型场景下 像 Tomcat、Dubbo 这类框架,它们内部的线程池主要用来处理网络 IO 任务的,所以它们都对 JUC 线程池的执行流程进行了调整来支持 IO 密集型场景使用 它们提供了阻塞队列 TaskQueue,该队列继承 LinkedBlockingQueue,重写了 offer() 方法来调整执行的流程,如下: 如果 parent 为 null,直接调用父类的 offer 方法,这里的 parent 就是所属的线程池对象 如果当前线程池等于最大线程数,则直接调用父类的 offer 方法入队 如果当前未执行的任务数量小于当前线程数,说明有空闲线程数,那么直接调用父类 offer 方法 入队 如果当前线程数小于最大线程数,则直接返回 false,然后回到 JUC 线程池的执行流程 其他情况都直接入队 如果 Tomcat 接收的请求数量大于核心线程数,请求就会被放到队列中,等待核心线程处理,如果并发量很大,就会在队列中堆积大量任务,这样会降低请求的总体响应速度 所以 Tomcat 并没有使用 JUC 原生线程池,而是利用 TaskQueue 的 offer() 方法巧妙的修改了 JUC 的执行流程 判断如果当前线程数 < 核心线程数,则新建一个线程来处理提交的任务 如果核心线程数 < 当前线程数 < 最大线程数,则创建新的线程执行提交的任务 如果当前线程数 >= 最大线程数,则将任务放入到任务队列待执行 如果队列已满,则执行拒绝策略 而且 Tomcat 会做核心线程预热,在创建好线程池接着会去创建核心线程并启动,服务启动后就可以接收客户端请求进行处理了,避免了冷启动问题 线程池的 Worker 线程模型,继承 AQS 实现了锁机制,线程启动后执行 runWorker() 方法,runWorker() 方法调用 getTask() 方法从阻塞队列中获取任务,获取到任务先执行 beforeExecute() 钩子函数,再执行任务,然后执行 afterExecute() 钩子函数,若超时获取不到任务会调用 processWorkerExit() 方法执行 Worker 线程的清理工作。 Java 官方提供了哪几种线程池,分别有什么特点?newCachedThreadPool,是一种可以缓存的线程池,它可以用来处理大量短期的突发流量 1. 它的特点有三个 1. 最大线程数是 Integer.MaxValue 2. 线程存活时间是 60 s 3. 阻塞队列用的是 SynchronousQueue,这是一种不存在任何元素的阻塞对象 4. 也就是每提交一个任务到线程池,都会分配一个工作线程来处理,由于最大线程数没有限制 5. 所以它可以处理大量的任务,另外每个工作线程又可以存活 60 s,使得这些工作线程可以缓存起来应对更多任务的处理2. newFixedThreadPool,是一种固定线程数量的线程池 1. 它的特点是核心线程和最大线程数量都是一个固定的值 2. 没有所谓的非空闲时间 3. 阻塞队列为无界队列 LinkedBlockingQueue,可能会导致 OOM 4. FixedThreadPool 适用于处理 CPU 密集型的任务,确保 CPU 在长期被工作线程使用的情况下,尽可能少的分配,即适用于执行长期的任务 5. 如果任务比较多工作线程处理不过来,就会加入到阻塞里面等待 6. 工作流程: 1. 提交任务,如果线程数少于核心线程数,创建核心线程执行任务 2. 如果线程数等于核心线程,把任务添加到 LinkedBlockingQueue 阻塞队列 3. 如果线程执行完任务,去阻塞队列去任务,继续执行3. newSingleThreadPool,只有一个工作线程的线程池 1. 并且线程数量无法动态更改,因此可以保证所有的任务都按照 FIFO 的方式顺序执行4. newScheduledThreadPool,具有延迟执行功能的线程池,适用与周期性执行任务的场景,需要限制线程数量的场景 1. 最大线程是 Integer.MAX_VALUE,也有 OOM 的风险 2. 阻塞队列是 DelayedWorkQueue,keepAliveTime 为 0 3. scheduleAtFixedRate():按某种速率周期执行 4. scheduleWithFixedDelay():在某个延迟后执行 5. 工作机制: 1. 线程从 DelayQueue 中获取已到期的 ScheduledFuture 2. 线程执行这个 ScheduledFutureTask 3. 线程修改 ScheduledFutureTask的 time 变量为下次将执行的时间 4. 线程把这个修改 time 之后的 ScheduledFutureTask 放回 DelayQueue 中5. newWorkStealingPool,Java8 里面新加入的一个线程池 1. 它内部会构建一个 ForkJoinPool,利用工作窃取的算法并行处理请求6. 这些线程池都是通过工具类 Executors 来构建的,线程池的最终实现类是 ThreadPoolExecutor。 线程池提交 execute 和 submit 有什么区别?从是否有返回值的角度来看,execute 用于提交不需要返回值的任务, submit() 方法用于提交需要返回值的任务,线程池会返回一个 future 的对象,通过这个 future 对象可以判断任务是否执行成功,并且可以通过 future 的 get 方法来获取返回值 execute() 无返回值,submit() 有返回值,会返回一个 FutureTask,然后调用 get() 方法阻塞获取返回值 FutureTask 实现了 RunnableFuture 接口,而 RunnableFuture 继承了 Runnbale、Future 接口,Runnbale 只定义了一个 run 抽象方法,并且接口上加了 @FunctionalInterface,表示该接口只能定义一个抽象方法 我们调用 submit() 方法提交的任务,会被包装为 FutureTask 对象,FutureTask 类提供了七种任务状态和五个成员变量 分别是 初始态的 NEW、任务执行完成,还未设置任务结果的 COMPLETING 、任务执行结束后的状态 NROMAL、任务执行中发生异常后的 EXCEPTIONAL、调用 cancel 方法执行成功后的 CANCELLED、任务正在被中断时的 INTERRUPTING 状态、任务执行被中断后的 INTERRUPTED 状态 volatile修饰的int类型的state变量、callable、Object 类型的 outcome、volatile 修饰的 Thread 的 runner 变量、volatile 修饰的 WaitNode 类型的 waiters, 创建 FutureTask 对象时 state 设置为 NEW、callable 赋值为我们传入的任务 run 方法中会去执行 callable 任务,执行之前会去判断任务处于 NEW 状态并且通过 cas 设置 runner 为当前线程成功,然后去调用 call方法 执行任务,执行成功后会调用 set 方法将结果赋值给 outcome 任务执行抛出异常会将异常信息调用 setException 赋值给 outcome 在 set 和 setException 这两个方法中,都会先将状态变为 COMPLETING,再变为 NORMAL 或者 EXCEPTIONAL,是为了保证 NORMAL 或者 EXCEPTIONAL 已经完成了 outcome 赋值 finishCompletion 会通过 LockSupport.unpark 去唤醒因调用 get 而阻塞的线程 waiters 调用 get 方法会阻塞获取结果,如果 state > COMPLETING,说明任务已经执行完成了,则直接通过 report() 方法返回结果或者抛出异常,反之 <= COMPLETING,说明任务还在执行中或者还未开始,则调用 awaitDone() 进行阻塞等待 awaitDone() 则通过 state 状态判断来决定直接返回还是将当前线程添加到 waiters 里,然后调用 LockSupport.park 方法挂起当前线程 还有个重要的 cancel 方法,如果 state 不是 NEW 或者通过 CAS 赋值给 INTERRUPTING/CANCELLED 失败则直接返回,反之如果 mayInterrupIfRunning = true,表示可能中断在运行中线程,则中断线程,state 变为 INTERRUPTED,最后去唤醒等待的线程 从提交的任务类型角度来看,execute() 只能提交 Runnable 类型的任务,而 submit() 既能提交 Runnable 类型的任务,也能提交 Callable 类型的任务 从 API 层面理解,execute() 是在 Executor 接口中定义的,而submit() 方法是在 ExecutorService 接口中定义的,具体的实现是由 AbstarctExecutorService 进行 从处理异常的角度来看,execute 会直接抛出任务执行时的异常,可以使用 try catch 来捕获,和普通线程的处理方式完全一致,而 submit 会吃掉异常,可以通过 Future 的 get 方法将任务执行时的异常重新抛出。

★★★★★

一本不错的书,耐着性子读完了,对于多线程,集合有了更深刻的认识。作者不仅仅讲述了技术的原理还对其技术思想进行了讲解,知其然知其所以然。美中不足的是内容难度大,没照顾好一部分新手(说的就是我了),阅读起来理解难度挺大,除此之外,本书的风格太过干,阅读起来很枯燥,比较考验耐心。

★☆☆☆☆

深度不够,通篇API,Vector 真的没必要再讲了

★★★★★

写得好呀,帮我梳理清楚了很多问题

★★★★★

值得品读,除了个别地方不够严谨外,内容挺有深度,甚至比其他专门写高并发的书写得更深入

书籍信息

作者银文杰
出版社电子工业出版社
出版时间2021-12-01
ISBN9787121422652
分类计算机-编程设计
格式EPUB
完结状态连载中
章节数16