并发编程可以总结为3个核心问题:
- 分工:指的是如何高效的拆解任务并分配给线程
- 同步:指的是线程之间如何协作
- 互斥:则是保证同一时刻只允许一个线程访问共享资源
JavaSDK并发包很大部分都是按照这三个维度组织的:
进一步的,个人理解,三大核心问题又可以聚焦于2大点:
- 管程(synchronized、SDK里的各种Lock)解决同步互斥问题
- 线程池(CompletableFuture、Fork/Join)实现分工并行
抓住这2点,就可以由点及线,把整个并发编程体系串联起来。个人理解,其实线程池部分严格说来应该属于并行编程。
ps.并发 VS 并行 ?
- 并发,指的是某一时间段内,单核CPU多任务处理,因为线程调度切换使得看起来是同时执行多任务一样,实际上某一时间只有一个线程运行。
- 并行,指的是多CPU情况下,同一时间点多个线程同时运行。
管程
管程是一把解决并发问题的万能钥匙。所谓管程,指的是管理共享变量以及对共享变量的操作过程,让他们支持并发。翻译为 Java 领域的语言,就是管理类的成员变量和成员方法,让这个类是线程安全的。
在管程模型里,共享变量和对共享变量的操作是被封装起来的,当多个线程同时试图进入管程内部时,只允许一个线程进入,其他线程则在入口等待队列中等待。 ----解决了互斥
条件变量和等待队列的作用----解决线程同步问题
- 多个线程同时试图进入管程内部时,只允许一个线程进入,其他线程则在入口等待队列中等待(处于阻塞状态)。
- T1线程进入后,检查条件变量A,如满足,则执行。如不满足,T1 需要调用 A.wait()进入条件变量A的等待队列(释放了锁,处于无限等待状态/有限时等待状态),进入之后,允许其他线程进入管程。
- 另外一个线程 T2 执行成功之后,如果条件A对于线程 T1 来说已经满足了,此时线程 T2 要通知 T1,线程 T2 需要调用 A.notifyAll() 来唤醒 A 等待队列中的线程,当这些线程得到通知后,会从等待队列里面出来,但是出来之后不是马上执行,而是重新进入到入口等待队列里面去竞争锁,线程是否竞争到锁与线程在等待队列中的先后位置是否相关,又产生了非公平锁和公平锁之区分。
java语言内置的管程(synchronized)对 MESA 管程模型进行了精简。MESA 模型中,条件变量可以有多个,synchronized只有一个条件变量,只支持非公平锁。
正是由于synchronized的局限性,才有了JUC包中的各种lock,JUC包中的AQS就是对MESA管程的实现,然后基于AQS延伸开发出了各种lock和并发工具类,其核心思想的源头都是管程!
线程池
降低延迟,提高吞吐量
线程池基本原理:美团的这篇文章讲的很好---Java线程池实现原理及其在美团业务中的实践
多线程优化性能的目标说白了就是2点,降低延迟,提高吞吐量,具体做法就是串改并,串行转换成并行的过程中,一定会涉及到异步化。异步化,是并行方案得以实施的基础。
-
简单的并行任务-----------------------------------------> 线程池 +Future
-
任务之间有聚合关系(AND 聚合/OR 聚合)--------------------> CompletableFuture (强烈推荐使用!!!)
-
分治 -------------------------------------------------> Fork/Join (适合处理一些特殊场景,并行计算 海量数据处理)
一些线程池的骚操作:
美团动态线程池
按某个维度顺序执行任务的线程池
内存安全的阻塞队列
原生线程池拒绝策略的bug