查看原文
其他

超实用系列:Java并发工具全图鉴(必收藏)

悟空 imooc编程手记 2022-06-20

作者 | 悟空 慕课网新星讲师

来源 | 慕课网(imooc.com)




  前言:


面试久了,你会发现Java并发编程是面试的重点考察点。并且随着公司的发展,用户量级会与日俱增,高性能、高并发的问题自然避免不了,因此具备并发处理能力的 Java 程序员必然是广大公司渴求的香饽饽。


接下来和大家分享我对Java并发知识的见解,全文主要分为如下7大点:

1. Java并发工具的痛点
2. 三大类:线程安全、方便管理、互相配合
3. 第一类:为了线程安全
4. 第二类:为了管理线程
5. 第三类:为了线程协作--控制并发流程的工具类
6. 面试常见问题
7. 继续深入学习的途径


1、Java并发工具痛点


Java并发中,一块非常重要的内容就是对于各种工具类的理解,常见的广义的并发工具包括:线程池、ConcurrentHashMap、AtomicInteger、CopyOnWriteArrayList、ArrayBlockingQueue、synchronized、LongAdder、ThreadLocal、各种Lock、Future、CountDownLatch、Semaphore等等。


以上这些工具,大家或多或少肯定都使用过,但是如果让我们自己编写功能类似的工具类,就很容易出错,因为我们可能会考虑不周;而JDK的常用工具类是经过了千万人检验的,值得信赖且功能完善。


我们应该学会使用,并深入理解这些工具类的原理。非必须情况下,没有必要重复造轮子,因为如果让我们自己写一个经得起考验的功能完善的线程池,那其实是非常困难的,有几千行代码,实际上,能完全读懂就已经不太容易了。


从刚才那一长串的工具类的名字中可以看出,并发工具的数量很多,而且功能好像也不尽相同,不容易完全掌握。


所以我们在此,对Java中常见的工具类做一个梳理,把它们排排坐分分类,以便啃下这块“硬骨头”。


2、Java工具三大类


Java并发工具按照目的,一共分为3类:
第一类:为了线程安全
第二类:为了管理线程
第三类:为了线程协作——控制并发流程

具体展开请点击下方的思维导图↓↓↓

图1 并发工具类的分类


有了这样的分类,未来我们拿到一个不熟悉的并发工具类的时候,就可以对号入座。先建立起全局的概念,未来的学习就会容易很多。

下面我们对这三个类别分别展开描述。



3、第一类:为了线程安全


第一类是最常见的工具类,最典型的有各种锁、原子类、ConcurrentHashMap等,这些工具的目的都是帮助我们在并发的情况下保证线程安全。


就达到线程安全的工具类而言,有两个分类的角度,第一个是从底层原理来分,第二个是从使用者程序员的角度来分类。这两种分类只是视角不同,但是本质相同,是一一对应的关系。


图2 达到并发安全的多种手段



在此,我们以线程安全里的老大哥“锁”为例,来一探究竟:


1   锁的分类——各种类型的锁很多,乱花渐欲迷人眼,如何拨开迷雾,看透本质?


下图这些分类,是从各种不同角度出发去看的,但是这些分类并不是互斥的,也就是多个类型可以并存:有可能一个锁,同时属于两种类型,比如ReentrantLock既是互斥锁,又是可重入锁。


 图3 Java锁的分类



2   常见的锁分别属于哪类?适用场景是什么?


可以看出,通常我们把锁分为以下这些类别:

  1. 悲观锁和乐观锁

  2. 共享锁和独占锁

  3. 公平锁和非公平锁

  4. 可重入锁和非可重入锁

  5. 可中断锁和不可中断锁

  6. 自旋锁和非自旋锁

 

我们以3个最常用的锁为例子,分析类型和适用场景如下:



属于哪类锁

适用场景

ReentrantLock(实现Lock接口)

  • 悲观锁

  • 独占锁

  • 可以自由设置是否是公平锁

  • 可重入锁

  • 可中断锁(有lockInterruptibly等可中断方法)

  • CAS获取锁的时候有自旋(等学了源码分析后,再更新)

ReentrantLock功能强大,适用于大部分的普通情况

synchronized关键字

  • 同时是悲观锁(重量级锁)和乐观锁(轻量级锁)

  • 独占锁

  • 非公平锁

  • 可重入锁

  • 不可中断锁

  • 自旋锁(轻量级锁的时候)

  • 在偏斜锁、轻量级锁、重量级锁之间升降级

对于Lock和synchronized的选择标准,参加本文本末的“继续学习”中的免费课部分

ReentrantReadWriteLock(实现ReadWriteLock接口)

  • 读锁是共享锁,写锁是独占锁

  • 可以自由设置是否是公平锁

  • 可重入锁,顾名思义

  • 可中断锁(有lockInterruptibly等可中断方法)

ReentrantReadWriteLock适用于读多写少的情况,合理使用可以进一步提高并发效率。



4、第二类:为了管理线程


除了刚才说到的为了线程安全的工具类之外,现在将要介绍的“管理线程”也是很重要的一类并发工具,最典型的就是线程池和Future相关类,我们先来看看线程池。

为了方便管理线程,提高效率——线程池


1    线程池的重要性

线程池是非常重要的工具,如果你要成为一个好的工程师,需要比较好地掌握这个知识,很多线上问题都是因为没有用好线程池所导致的。即使你现阶段的目标还未考虑那么深远,也要知道,这基本上是面试必问的题目,而且面试官很容易从被面试者的回答中捕捉到被面试者的技术水平。


2    什么是“池”

软件中的“池”,可以理解为工厂,工厂里有一定数量的工人,工人会帮助你制造产品,但是每一个工人的招聘和培训都要花费很大成本,所以你希望尽量利用这已有的例如20个工人长期帮你做事,而不是每次都重新招聘。


如果最近生意好,我们可以适当扩招,但是工厂所能被分配到的资源是有限的,比如占地面积、资金等,所以当你真的接到过多订单的时候,你没办法无限扩招工人,所以订单只能排队,等待工人慢慢处理。

 

总结成两点,“池”的作用:

· 复用已有资源

· 控制资源总量

数据库连接池是这样,线程池也是如此。

 

当一个新任务过来了,我发现池里有空闲的线程,我就直接派他去干活,不需要重新创建一条线程了,要知道线程的创建和销毁可都是麻烦事;而如果新任务过来的时候,如果池里的线程都在忙,并且现在池子的线程已经很多了,那么新任务就去排队~


3    为什么要使用线程池

总而言之是减小开销,提高效率:


1) 问题一:反复创建线程开销大

在Java中,如果每个请求到达就去创建一个新线程,开销是相当大的,因为每一个Java的线程就对应一个操作系统的线程。在实际使用中,线程的创建和销毁都是需要时间的,如果是一个量级非常轻的请求,服务器也要新创建一条线程去处理,那么有可能创建和销毁线程消耗的时间,比请求处理的时间还更长。


2) 问题二:过多的线程会占用太多内存

创建过多的线程还会导致内存溢出。活动的线程需要消耗系统资源,如果在一个JVM里创建太多的线程,可能会使系统由于过度消耗内存或“切换过度”而导致系统资源不足。


3) 解决以上两个问题的思路

尽可能使用少量的线程——避免内存占用过多

让这部分线程都保持工作,且可以反复执行任务——避免生命周期的损耗


4    线程池好处


· 线程池解决了线程生命周期开销问题和系统资源不足的问题


线程池刚创建的时候,会先创建一定数量的线程,比如说10个,这样当有新请求分配过来的时候,就可以直接从池子里取出一个已经创建好的线程,直接开始处理请求,这样就省去了创建线程的时间。


再加上对线程的重复使用,就可以大大减小了线程生命周期的开销,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟,使用应用程序响应更快,增强了用户体验。

 

· 合理统筹内存和CPU的使用


通过灵活适当地调整线程中的线程数目,使得既不会由于线程太多导致内存溢出(避免抛出java.lang.OutOfMemoryError: unable to create new native thread),也不会由于线程太少导致浪费CPU资源,而是可以达到完美的平衡。

 

·  统一管理资源


使用线程池可以统一管理任务队列和线程,例如可以统一开始和结束,比单个线程逐一处理任务要更方便、更易于管理。同时这样也便于数据统计,因为每个ThreadPoolExecutor还维护一些基本统计数据,例如已完成任务的数量。


5    线程池适合应用的场合

当一个服务器接受到大量短小线程的请求时,使用线程池技术是非常合适的,它可以大大减少线程的创建和销毁次数,提高服务器的工作效率。

 

获取子线程的运行结果——Future相关类


Future的核心思想是:一个方法的计算过程可能非常耗时,一直在原地等待方法返回,显然不明智。可以把该计算过程放到线程池去执行,并通过Future去控制方法的计算过程,在计算出结果后直接获取该结果,这样就避免了傻傻等待造成的时间浪费。


在JDK 8中,还引入了很好用的CompletableFuture,可以完成例如“等待多个异步任务完成后,获取到结果,再利用这些结果执行接下来的任务”等更复杂的功能。


5、第三类:为了线程协作


除了以上讲的“为了线程安全”、“为了管理线程”外,第三类并发工具类就是为了线程之间的协作互助。


控制并发流程的工具类,作用就是帮助我们程序员更容易得让线程之间开展合作,让线程之间相互配合,来满足业务逻辑,比如让线程A等待线程B执行完毕后再执行任务等合作策略。


控制并发流程的工具类简称同步工具类,主要有以下几个:


作用

说明

Semaphore

信号线,可以通过控制“许可证”的数量,来保证线程之间的配合

线程只有在拿到“许可证”后才能继续运行。相比于其他的同步器,更灵活

CyclicBarrier

线程会等待,直到足够多线程达到了事先规定的数目。一旦达到触发条件,就可以进行下一步的动作。

适用于线程之间相互等待处理结果就绪的场景

Phaser

和CyclicBarrier类似,但是计数可变

Java 7加入的

CountDownLatch

和CyclicBarrier类似,数量递减到0时,触发动作

不可重复使用

Exchanger

让两个线程在合适时交换对象

适用场景:当两个线程工作在同一个类的不同实例上时,用于交换数据

Condition

可以控制线程的“等待”和“唤醒”

是Object.wait(), notify(), notifyAll()的升级版,功能更强大,也更安全



6、面试常见问题


关于Java并发工具类和原理,每一个类都有非常多的知识点将会作为面试时常见的问题。


在此我们仅以ConcurrentHashMap为例,问题的难度会逐渐增大:


· 对比Hashtable, HashMap, TreeMap,它们有什么不同?

· 同样是线程安全,ConcurrentHashMap和Hashtable 的区别?

· 为什么有Collections.synchronizedMap(),还需要ConcurrentHashMap?

· HashMap(HashSet同理)在多线程下可能造成CPU100%,是为什么?

· JDK8的ConcurrentHashMap也会造成CPU 100%,你知道吗?

· HashMap在Java 7和8有什么不同?

· ConcurrentHashMap在Java 7和8有什么不同?画一下结构图

· 为什么Map桶中超过8个才转为红黑树?

· ConcurrentHashMap的扩容流程你了解吗?画一下流程图


7、继续深入学习路径


后续我将会对约二十种常见的并发工具类进行深入讲解,其中就包括本篇文章里含有的线程池和JUC中各种常用的类,会按照上面思维导图的思路层层展开。


小伙伴们如果对并发想更深入学习,可以扫码关注我哦~




● 必看!java后端,亮剑诛仙(最全知识点)

● 用完这几个沙雕软件,我开始怀疑沙雕的意义。。。

● 互联网人十二时辰,程序员看完哭了!

● 在《我的世界》里从零打造一台计算机有多难?复旦本科生大神花费了一年心血






Tips:

# 点下“在看”❤️

# 然后,留个言踩个楼?每月留言精选前五将有好礼相送哦

# 开奖日期:9月1号,奖品是精挑细选的技术书籍/专栏or慕课网精美周边

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存