Java并发编程(未完成,待继续)
最近学习了网易云课堂中的Java高性能编程课程,结合之前的一些学习和积累,希望通过本文对Java并发编程相关的知识做一个梳理,构建Java并发编程的知识体系。咳咳,本篇文章是我的第一篇创作,主要是为了帮助自己系统性的梳理Java并发编程先关的知识,如果有其他看客路过,欢迎进行分享和交流~~
正式开写前先说两点:
- 本文并不是进行知识的原创,而是对知识进行梳理,以形成体系。因此,文章本身可能更类似于大纲性质的知识描述,但在具体知识点上会提供相关的链接以便学习。换句话说,本文不生产知识,只是知识的搬运工。当然,分享的知识有好有坏,欢迎路过的看客能够分享更加优质的资源,先谢过了~~
- 基于第一点再说明下:学习知识 or 生产知识很重要,而梳理知识并形成知识体系也很重要,个人认为甚至更加重要。所以,我更希望自己能够梳理知识&构建体系,并针对具体的知识点提供相关连接,仅在必要的时候,自己才考虑知识的原创(希望不会有,嘎嘎~~)。当然,我知道对知识进行梳理并不简单,希望自己能够做的尽量好。
最后再强调下,互联网上的牛人很多,生产的优质资源也很多,我并不是其中之一,但是希望自己能够站在巨人的肩膀上!
OK,开工~~
本文主要对Java并发编程相关的知识点尝试总结和梳理,主要围绕“并发编程的基础”、“并发程序存在的问题”、“并发编程需要解决的问题”、“高性能并发编程”、“Java并发工具”、“Java并发容器”几个方面进行说明。具体如下:
并发编程的基础:Java并发程序的基础是线程,只有多线程才会产生并发,所以这一部分主要对Java线程进行了梳理,涉及Java线程模型、线程状态、线程创建&终止、线程池等知识。
并发程序存在的问题:这一部分描述了并发程序天然存在的3大问题(原子性、可见性、有序性),旨在让我们进行并发编程的过程中意识到这些问题,避免踩坑。同时,针对这3大问题,介绍了Java是如何解决的。
并发编程需要解决的问题:这一部分描述了我们在进行并发编程时面临的3个核心问题(分工、同步、互斥),以及我们如何解决这3个问题,搞定并发编程。
高性能并发编程:采用并发编程的目的是为了提高执行程序的性能,基于该点,本部分对如何进一步优化并发程序、提高程序性能进行了说明。主要涉及:如何合理创建线程、如何充分利用计算机资源、如何避免资源竞争、如何降低竞争开销 等几个方面。
Java并发工具&Java并发容器:这两部分主要是介绍JDK为我们提供了哪些工具,以便我们更加高效的进行并发编程&编写出更高效的并发程序。
OK,针对这6大部分,我们的详细整理如下。
一、并发编程的基础:Java线程
1、线程模型:
Java线程模型一般为一对一的内核线程模型,实际实现过程中的线程模型有如下3种:
1)内核线程模型:依赖操作系统实现&实现简单&存在操作系统的限制&线程切换开销大
- 一个用户线程对应一个轻量级进程(内核线程)
- 线程的最大个数受到操作系统限制
- 线程的调度完全依赖操作系统
- 线程的切换涉及到用户态与内核态的切换
2)用户线程模型:不依赖操作系统&实现复杂&线程切换开销低
- 多个用户线程对应一个进程
- 线程的最大个数由线程库的实现决定,能够支持大规模的线程数量
- 需要创建一个线程库,实现一套线程的创建、同步、销毁、调度机制,实现难度大
- 线程的切换不涉及用户态到内核态的切换,性能高
3)混合线程模型:
- N个用户线程对应M个轻量级进程(内核线程)
- 线程的最大个数由线程库的实现决定,能够支持大规模的线程数量
- 需要创建一个线程库,但可以使用系统内核提供的线程调度及处理器映射等功能,实现难度有所降低
- 线程的切换可能涉及到用户态与内核态的切换,也可能不涉及,性能较高
参考资料 https://www.cnblogs.com/kaleidoscope/p/9598140.html
2、线程状态&状态转换:
Java线程的线程状态包括:New、Runnable、Blocked、Waiting、Timed Waiting、Terminated,线程状态之间的转换如下图。(待补充)
参考资料 https://blog.csdn.net/shi2huang/article/details/80289155 、 https://www.jianshu.com/p/979ba2f919e3
3、线程创建方式:
1)继承Thread,重写run方法
2)实现Runnable接口及run方法,以Runnalbe实例为参数,创建Thread实例
3)实现Callable接口及call方法,以Callable实例为参数,创建FutureTask实例,再以FutureTask实例为参数,创建Thread实例。相比于前两个方式,第3个方式能够获取线程的执行结果或异常信息
4)利用线程池&提交任务的方式创建线程,结合 Executors/ThreadPoolExecutor/ExecutorService、Runnable、Callable、FutureTask等实现
5)通过Fork/Join框架创建线程,结合ForkJoinPool、ForkJoinTask等实现
参考资料 https://www.cnblogs.com/felixzh/p/6036074.html
4、线程挂起方式:
1)使用suspend&resume进行线程的挂起和唤醒:
- 已被弃用,因为不安全、易导致死锁
- 使用时suspend方法必须在resume方法之前被执行
- suspend方法将线程挂起后不会释放锁。
2)使用wait¬ify/notifyAll进行线程的挂起和唤醒:
- wait方法必须在notify/notifyAll方法之前被执行,否则线程会一直处于Waiting状态
- 使用时需要配合synchronized关键字使用
- 执行wait方法时会自动释放锁,并且被唤醒后会重新获得锁。
3)使用LockSupport.park&unpark进行线程的挂起和唤醒:
- 对两个方法执行的先后顺序没有要求,但是unpark的执行效果不会叠加,即多次调用unpark后,第一次调用park方法时线程不会挂起,但是第二次线程会挂起,可以理解为期间只是设置了一个标记位,没有叠加效果
- park方法不会释放锁,如果结合synchronized关键字使用时,要注意避免死锁问题。
4)使用Lock&Condition进行线程的挂起和唤醒:第4种方法是基于第3种方法实现的。
注意:实际编码的过程中,需要关注“伪唤醒”问题。所谓的“伪唤醒”是指线程并非由于notify、notifyAll、unpark等api调用而被唤醒,而是更加底层的原因导致,所以建议通过使用“循环+等待条件检查”来避免“伪唤醒”问题。
参考资料 (待补充)
5、线程终止的方式:
1)Thread的stop方法:强制终止线程的执行。该方法可能会打破线程执行的原子性,导致线程安全问题,如线程执行synchronized代码块的过程中被stop,会破坏代码块的原子性。
2)Thread的interrupt方法–线程中断:
- 该方法会设置线程的中断标志,但并不会终止线程的处理,实际使用的过程中,需要主动在代码中检查线程的中断标志并中断线程的处理
- 在线程处于非Blocked状态下执行interrupt方法时,会正常设置线程的中断标志
- 在线程处于Blocked状态下(wait、join、sleep…)执行interrupt方法时,线程的中断标志在设置后会被清除,同时抛出InterruptedException异常
3)标志位法:
- 代码逻辑中,增加一个判断,用来控制是否终止线程的执行
- 线程的执行逻辑应为 while+终止条件判断
参考资料 (待补充)
6、线程池:
1)什么是资源的池化?池化模型的作用?
主要是针对可以重复利用的资源,创建一个资源池将其保存起来,避免重复创建和释放资源带来的开销。常见的有对象池、线程池、数据库连接池等。
2)JDK提供的线程池
- 线程池的创建方式:JDK提供的线程池创建类为ThreadPoolExecutor,对应的参数说明如下
int corePoolSize–线程池的核心线程数
int maximumPoolSize–线程池的最大线程数
long keepAliveTime–非核心线程空闲时的最大存活时间
TimeUnit unit–存活时间的时间单位
BlockingQueue workQueue–任务队列,用于存储等待执行的任务的队列
ThreadFactory threadFactory–线程工厂,用于创建线程,一般默认即可
RejectedExecutionHandler handler–拒绝策略,当提交的任务过多而无法处理时,会使用拒绝策略处理提交的任务 - 4种默认的线程池:实际上是基于ThreadPoolExecutor进行了一些包装
Executors.newCachedThreadPool–线程池的大小无限制,任务队列为有界队列
Executors.newFixedThreadPool–线程池的大小固定,任务队列为无界队列
Executors.newSingleThreadPool–线程池的长度为1,任务队列为无界队列
Executors.newScheduledThreadPoolExecutor–线程池的大小无限制,任务队列为无界队列,可定时或周期性执行任务 - 4中默认的拒绝策略:
ThreadPoolExecutor.AbortPolicy–丢弃任务并抛出RejectedExecutionException异常
ThreadPoolExecutor.DiscardPolicy–丢弃任务,但是不抛出异常
ThreadPoolExecutor.DiscardOldestPolicy–丢弃最老的任务,将新的任务加入到等待队列
ThreadPoolExecutor.CallerRunsPolicy–由调用线程处理该任务,线程池不进行处理 - 任务提交方式:
execute–线程池执行完任务后,不会有返回信息,也拿不到异常信息
submit-- 可用于在线程池执行完任务后,获取执行结果或异常信息
3)线程池的执行逻辑
- 线程数小于核心线程数时,创建线程执行任务
- 线程数大于核心线程数&任务队列未满时,任务进入队列进行等待
- 线程数大于核心线程数&任务队列已满&线程数小于最大线程数时,创建线程执行任务
- 线程数大于核心线程数&任务队列已满&线程数大于最大线程数时,执行拒绝策略
4)如何合理设置线程池的大小
- 参见高性能并发编程小结的说明(CPU密集型任务、IO密集型任务)
参考资料 (待补充)
=============================================================================== 华丽的分割线,下面的内容待完善
二、并发程序存在的问题:并发问题/多线程问题、3大源头
1、什么是并发问题
1)共享变量&非共享变量
2)栈封闭&ThreadLocal
2、并发问题的3大源头
1)哪三大问题:原子性问题、可见性问题、有序性问题
2)为什么会有这3大问题:编译优化&缓存
3、如何解决3大问题
1)原子性问题:锁,synchronized关键字 对比 Lock
2)可见性问题:volatile关键字、happens-before规则
3)有序性问题:volatile关键字、final关键字…
4)Java内存模型
三、并发编程需要解决的问题:分工、同步、互斥
1、任务分工:
Executor、Fork/Join、Future,生产者-消费者模式、Thread-Per-Message模式、Worker Thread模式
2、线程间的通信(同步):
1)文件共享
2)网络共享
3)变量共享(共享内存)
4)线程协作API(阻塞&唤醒)
- suspend&resume
- wait¬ify/notifyAll
- LockSupport.park&unpark
- Lock&condition
- 同步工具类:CountDownLatch、CyclicBarrier
3、资源的互斥访问(共享资源、共享变量):
1)无锁
- 不可变量final
- 线程本地变量-ThreadLocal(内存泄漏问题)
- COW
- CAS无锁并发
2)互斥锁
- synchronized
- Lock
- 读写锁
- 死锁问题
四、高性能并发线程
1、尽可能的避免竞争
1)不可变模式
2)线程本地变量
3)COW
2、尽可能降低竞争开销
1)无锁并发(乐观锁&悲观锁)
2)降低加锁范围、减少锁的持有时间(粗粒度锁&细粒度锁)
3)读写锁(共享锁&排他锁)
3、尽可能提升资源利用率
1)创建合适的线程个数:CPU密集型任务、IO密集型任务
2)异步编程:Guarded Suspension模式、Future模式…
3)协程:降低线程的资源消耗&线程切换的消耗
4、扩展–高性能编程技术
1)资源池化
2)请求缓冲
3)请求聚合(合并)
4)数据缓存
5)任务拆解(多线程)
6)异步编程
7)负载均衡
8)分布式
五、Java并发工具类
1、分工
1)CompletableFuture
2)CompletionService
3)Fork/Join
2、同步
1)Lock&Condition
2)Semaphore
3)CountDownLatch
4)CyclicBarrier
3、互斥
1)ReadWriteLock
2)StampedLock
4、AQS
六、Java并发容器
1、List
1)CopyOnWriteArrayList
2、Map
1)ConcurrentHashMap
2)ConcurrentSkipListMap
3、Set
1)CopyOnWriteArraySet
2)CopyOnWriteSkipListSet
4、Queue
1)有界
a、ArrayBlockingQueue
b、LinkedBlockingQueue
c、SynchronousQueue
2)无界
a、PriorityBlockingQueue
b、DelayQueue
c、ConcurrentLinkedQueue