1.
什么是线程池?
Java
中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用
线程池。在开发过程中,合理地使用线程池能够带来许多好处。
降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降
低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用
2.
线程池作用?
线程池是为突然大量爆发的线程设计的,通过有限的几个固定线程为大量的操作服务,减少了创建
和销毁线程所需的时间,从而提高效率。
如果一个线程所需要执行的时间非常长的话,就没必要用线程池了
(
不是不能作长时间操作,而是
不宜。本来降低线程创建和销毁,结果你那么久我还不好控制还不如直接创建线程
)
,况且我们还
不能控制线程池中线程的开始、挂起、和中止。
3.
线程池有什么优点?
降低资源消耗:重用存在的线程,减少对象创建销毁的开销。
提高响应速度。可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,
避免堵塞。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统
的稳定性,使用线程池可以进行统一的分配,调优和监控。
附加功能:提供定时执行、定期执行、单线程、并发数控制等功能。
4.
什么是
ThreadPoolExecutor
?
ThreadPoolExecutor
就是线程池
ThreadPoolExecutor
其实也是
JAVA
的一个类,我们一般通过
Executors
工厂类的方法,通过传入
不同的参数,就可以构造出适用于不同应用场景下的
ThreadPoolExecutor
(线程池)
构造参数参数介绍:
corePoolSize
核心线程数量
maximumPoolSize
最大线程数量
keepAliveTime
线程保持时间,
N
个时间单位
unit
时间单位(比如秒,分)
workQueue
阻塞队列
threadFactory
线程工厂
handler
线程池拒绝策略
5.
什么是
Executors
?
Executors
框架实现的就是线程池的功能。
Executors
工厂类中提供的
newCachedThreadPool
、
newFixedThreadPool
、
newScheduledThreadPool
、
newSingleThreadExecutor
等方法其实也只是
ThreadPoolExecutor
的构造函数参数不同而已。通过传入不同的参数,就可以构造出适用于不同
应用场景下的线程池
6.
线程池四种创建方式?
Java
通过
Executors
(
jdk1.5
并发包)提供四种线程池,分别为:
1. newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回
收空闲线程,若无可回收,则新建线程。
2. newFixedThreadPool
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列
中等待。
3. newScheduledThreadPool
创建一个定长线程池,支持定时及周期性任务执行。
4. newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任
务,保证所有任务按照指定顺序
(FIFO, LIFO,
优先级
)
执行。
7.
在
Java
中
Executor
和
Executors
的区别?
Executors
工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。
Executor
接口对象能执行我们的线程任务。
ExecutorService
接口继承了
Executor
接口并进行了扩展,提供了更多的方法我们能获得任务执
行的状态并且可以获取任务的返回值。
使用
ThreadPoolExecutor
可以创建自定义线程池。
8.
四种构建线程池的区别及特点?
1. newCachedThreadPool
特点
:
newCachedThreadPool
创建一个可缓存线程池,如果当前线程池的长度超过了处理的需要
时,它可以灵活的回收空闲的线程,当需要增加时, 它可以灵活的添加新的线程,而不会对池的
长度作任何限制
缺点
:他虽然可以无线的新建线程,但是容易造成堆外内存溢出,因为它的最大值是在初始化的时
候设置为
Integer.MAX_VALUE
,一般来说机器都没那么大内存给它不断使用。当然知道可能出问
题的点,就可以去重写一个方法限制一下这个最大值
总结
:线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线
程,而不用每次新建线程。
2.newFixedThreadPool
特点
:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。定长线程池的
大小最好根据系统资源进行设置。
缺点
:线程数量是固定的,但是阻塞队列是无界队列。如果有很多请求积压,阻塞队列越来越长,
容易导致
OOM
(超出内存空间)
总结
:请求的挤压一定要和分配的线程池大小匹配,定线程池的大小最好根据系统资源进行设置。
如
Runtime.getRuntime().availableProcessors()
3.newScheduledThreadPool
特点
:创建一个固定长度的线程池,而且支持定时的以及周期性的任务执行,类似于
Timer
(
Timer
是
Java
的一个定时器类)
缺点
:由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一
个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务(比如:一个任务出错,以后的
任务都无法继续)。
4.newSingleThreadExecutor
特点
:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,如果这个唯一的线程因
为异常结束,那么会有一个新的线程来替代它,他必须保证前一项任务执行完毕才能执行后一项。
保证所有任务按照指定顺序
(FIFO, LIFO,
优先级
)
执行。
缺点
:缺点的话,很明显,他是单线程的,高并发业务下有点无力
总结
:保证所有任务按照指定顺序执行的,如果这个唯一的线程因为异常结束,那么会有一个新的
线程来替代它
9.
线程池都有哪些状态?
RUNNING
:这是最正常的状态,接受新的任务,处理等待队列中的任务。
SHUTDOWN
:不接受新的任务提交,但是会继续处理等待队列中的任务。
STOP
:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。
TIDYING
:所有的任务都销毁了,
workCount
为
0
,线程池的状态在转换为
TIDYING
状态时,会
执行钩子方法
terminated()
。
TERMINATED
:
terminated()
方法结束后,线程池的状态就会变成这个。
10.
线程池中
submit()
和
execute()
方法有什么区别?
相同点:
相同点就是都可以开启线程执行池中的任务。
不同点:
接收参数:
execute()
只能执行
Runnable
类型的任务。
submit()
可以执行
Runnable
和
Callable
类型的任务。
返回值:
submit()
方法可以返回持有计算结果的
Future
对象,而
execute()
没有
异常处理:
submit()
方便
Exception
处理
11.
什么是线程组,为什么在
Java
中不推荐使用?
ThreadGroup
类,可以把线程归属到某一个线程组中,线程组中可以有线程对象,也可以有线程
组,组中还可以有线程,这样的组织结构有点类似于树的形式。
线程组和线程池是两个不同的概念,他们的作用完全不同,前者是为了方便线程的管理,后者是为
了管理线程的生命周期,复用线程,减少创建销毁线程的开销。
为什么不推荐使用线程组?因为使用有很多的安全隐患吧,没有具体追究,如果需要使用,推荐使
用线程池。
12. ThreadPoolExecutor
饱和策略有哪些?
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,
ThreadPoolTaskExecutor
定
义一些策略
:
ThreadPoolExecutor.AbortPolicy
:抛出
RejectedExecutionException
来拒绝新任务的处理。
ThreadPoolExecutor.CallerRunsPolicy
:调用执行自己的线程运行任务。您不会任务请求。但是
这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。
如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策
略。
ThreadPoolExecutor.DiscardPolicy
:不处理新任务,直接丢弃掉。
ThreadPoolExecutor.DiscardOldestPolicy
: 此策略将丢弃最早的未处理的任务请求。
13.
如何自定义线程线程池
?
先看
ThreadPoolExecutor
(线程池)这个类的构造参数
corePoolSize
核心线程数量
maximumPoolSize
最大线程数量
keepAliveTime
线程保持时间,
N
个时间单位
unit
时间单位(比如秒,分)
workQueue
阻塞队列
threadFactory
线程工厂
handler
线程池拒绝策略
14.
线程池的执行原理?
提交一个任务到线程池中,线程池的处理流程如下:
1.
判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没
有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个
流程。
2.
线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队
列里。如果工作队列满了,则进入下个流程。
3.
判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任
务。如果已经满了,则交给饱和策略来处理这个任务。
15.
如何合理分配线程池大小
?
要合理的分配线程池的大小要根据实际情况来定,简单的来说的话就是根据
CPU
密集和
IO
密集来
分配
什么是
CPU
密集
CPU
密集的意思是该任务需要大量的运算,而没有阻塞,
CPU
一直全速运行。
CPU
密集任务只有在真正的多核
CPU
上才可能得到加速
(
通过多线程
)
,而在单核
CPU
上,无论你开
几个模拟的多线程,该任务都不可能得到加速,因为
CPU
总的运算能力就那样。
什么是
IO
密集
IO
密集型,即该任务需要大量的
IO
,即大量的阻塞。在单线程上运行
IO
密集型的任务会导致浪费
大量的
CPU
运算能力浪费在等待。所以在
IO
密集型任务中使用多线程可以大大的加速程序运行,
即时在单核
CPU
上,这种加速主要就是利用了被浪费掉的阻塞时间。
分配
CPU
和
IO
密集:
1. CPU
密集型时,任务可以少配置线程数,大概和机器的
cpu
核数相当,这样可以使得每个线程都在
执行任务
2. IO
密集型时,大部分线程都阻塞,故需要多配置线程数,
2*cpu
核数
精确来说的话的话:
从以下几个角度分析任务的特性:
任务的性质:
CPU
密集型任务、
IO
密集型任务、混合型任务。
任务的优先级:高、中、低。
任务的执行时间:长、中、短。
任务的依赖性:是否依赖其他系统资源,如数据库连接等。
可以得出一个结论:
线程等待时间比
CPU
执行时间比例越高,需要越多线程。
线程
CPU
执行时间比等待时间比例越高,需要越少线程。
16.
你经常使用什么并发容器,为什么?
答:
Vector
、
ConcurrentHashMap
、
HasTable
一般软件开发中容器用的最多的就是
HashMap
、
ArrayList
,
LinkedList
,等等
但是在多线程开发中就不能乱用容器,如果使用了未加锁(非同步)的的集合,你的数据就会非常
的混乱。由此在多线程开发中需要使用的容器必须是加锁(同步)的容器。
17.
什么是
Vector
Vector
与
ArrayList
一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一
个线程能够写
Vector
,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,访问它
比访问
ArrayList
慢很多
(
ArrayList
是最常用的
List
实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。当从
ArrayList
的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找
和遍历,不适合插入和删除。
ArrayList
的缺点是每个元素之间不能有间隔。
)
18.
用过
ConcurrentHashMap
,讲一下他和
HashTable
的不同之处?
ConcurrentHashMap
是
Java5
中支持高并发、高吞吐量的线程安全
HashMap
实现。它由
Segment
数组结构和
HashEntry
数组结构组成。
Segment
数组在
ConcurrentHashMap
里扮演锁的角色,
HashEntry
则用于存储键
-
值对数据。一个
ConcurrentHashMap
里包含一个
Segment
数组,
Segment
的结构和
HashMap
类似,是一种数组和链表结构;一个
Segment
里包含一个
HashEntry
数组,每个
HashEntry
是一个链表结构的元素;每个
Segment
守护着一个
HashEntry
数组里的元
素,当对
HashEntry
数组的数据进行修改时,必须首先获得它对应的
Segment
锁。
看不懂???很正常,我也看不懂
总结:
1. HashTable
就是实现了
HashMap
加上了
synchronized
,而
ConcurrentHashMap
底层采用分
段的数组
+
链表实现,线程安全
2. ConcurrentHashMap
通过把整个
Map
分为
N
个
Segment
,可以提供相同的线程安全,但是效
率提升
N
倍,默认提升
16
倍。
3.
并且读操作不加锁,由于
HashEntry
的
value
变量是
volatile
的,也能保证读取到最新的值。
4. Hashtable
的
synchronized
是针对整张
Hash
表的,即每次锁住整张表让线程独占,
ConcurrentHashMap
允许多个修改操作并发进行,其关键在于使用了锁分离技术
5.
扩容:段内扩容(段内元素超过该段对应
Entry
数组长度的
75%
触发扩容,不会对整个
Map
进
行扩容),插入前检测需不需要扩容,有效避免无效扩容
19. SynchronizedMap
和
ConcurrentHashMap
有什么区别?
SynchronizedMap
一次锁住整张表来保证线程安全,所以每次只能有一个线程来访为
map
。
ConcurrentHashMap
使用分段锁来保证在多线程下的性能。
ConcurrentHashMap
中则是一次锁住一个桶。
ConcurrentHashMap
默认将
hash
表分为
16
个
桶,诸如
get
,
put
,
remove
等常用操作只锁当前需要用到的桶。
这样,原来只能一个线程进入,现在却能同时有
16
个写线程执行,并发性能的提升是显而易见
的。
另外
ConcurrentHashMap
使用了一种不同的迭代方式。在这种迭代方式中,当
iterator
被创建后
集合再发生改变就不再是抛出
ConcurrentModificationException
,取而代之的是在改变时
new
新的数据从而不影响原有的数据,
iterator
完成后再将头指针替换为新的数据 ,这样
iterator
线程
可以使用原来老的数据,而写线程也可以并发的完成改变。
20. CopyOnWriteArrayList
是什么
?
CopyOnWriteArrayList
是一个并发容器。有很多人称它是线程安全的,我认为这句话不严谨,缺
少一个前提条件,那就是非复合场景下操作它是线程安全的。
CopyOnWriteArrayList(
免锁容器
)
的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛
出
ConcurrentModificationException
。在
CopyOnWriteArrayList
中,写入将导致创建整个底层
数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。
21. CopyOnWriteArrayList
的缺点
?
由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致
young gc
或者
full gc
。
不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个
set
操作后,读取到数
据可能还是旧的,虽然
CopyOnWriteArrayList
能做到最终一致性
,
但是还是没法满足实时性要求。
由于实际使用中可能没法保证
CopyOnWriteArrayList
到底要放置多少数据,万一数据稍微有点
多,每次
add/set
都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种
操作分分钟引起故障。
22.
常用的并发工具类有哪些?
CountDownLatch
CountDownLatch
类位于
java.util.concurrent
包下,利用它可以实现类似计数器的功能。比如有
一个任务
A
,它要等待其他
3
个任务执行完毕之后才能执行,此时就可以利用
CountDownLatch
来
实现这种功能了。
CyclicBarrier (
回环栅栏
) CyclicBarrier
它的作用就是会让所有线程都等待完成后才会继续下一步行
动。
CyclicBarrier
初始化时规定一个数目,然后计算调用了
CyclicBarrier.await()
进入等待的线程数。
当线程数达到了这个数目时,所有进入等待状态的线程被唤醒并继续。
CyclicBarrier
初始时还可带一个
Runnable
的参数, 此
Runnable
任务在
CyclicBarrier
的数目达到
后,所有其它线程被唤醒前被执行。
Semaphore (
信号量
) Semaphore
是
synchronized
的加强版,作用是控制线程的并发数量(允许
自定义多少线程同时访问)。就这一点而言,单纯的
synchronized
关键字是实现不了的。