天天看点

JUC和线程池相关面试题

1.线程安全集合之实现ConrrentHashMap

锁分段技术

HashTable容器在竞争激烈的并发环境下表现出效率低下的原因,是因为所有访问HashTable的线程都必须竞争同一把锁,那 假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存 在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段 的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。有 些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有 段,操作完毕后,又按顺序释放所有段的锁。这里“按顺序”是很重要的,否则极有可能出现死锁,在ConcurrentHashMap内 部,段数组是final的,并且其成员变量实际上也是final的,但是,仅仅是将数组声明为final的并不保证数组成员也是final的,这需要实现上的保证。这可以确保不会出现死锁,因为获得锁的顺序是固定的。

ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。

ConcurrentHashMap本质上是一个Segment数组,而一个Segment实例又包含若干个桶,每个桶中都包含一条由若干个HashEntry 对象链接起来的链表。总的来说,ConcurrentHashMap的高效并发机制是通过以下三方面来保证的(具体细节见后文阐述):

通过锁分段技术保证并发环境下的写操作;

通过HashEntry的不变性、Volatile变量的内存可见性和加锁重读机制保证高效、安全的读操作; 通过不加锁和加锁两种方案控制跨段操作的的安全性。

ConcurrentHashMap读操作不需要加锁的奥秘在于以下三点: 用HashEntery对象的不变性来降低读操作对加锁的需求;

用Volatile变量协调读写线程间的内存可见性; 若读时发生指令重排序现象,则加锁重读;

1.为什么要使用线程池?

简单的构建方式

每当一个请求到达就为其创建一个新线程,然后在新线程中为其服务。 优点:适用于原型开发

缺点:用于服务器存在诸多问题,为每个请求创建一个新线程的开销很大;花费在创建和销毁新线程上的时间和资源比花在处 理实际的用户请求上的时间和资源更多;除了创建和销毁线程的开销之外,活动的线程也消耗资源,在JVM中创建过多的线程会 导致系统过度消耗内存而用完内存或者"切换过度"。

线程的作用

主要用来解决线程生命周期开销问题和系统资源不足的问题。通过对多个任务重用线程,线程创建的开销就分摊到多个 任务上了,而且由于在请求到达时线程已经存在,所以消除了创建线程带来的延迟。

线程池作用就是限制系统中执行线程的数量。根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果;少 了浪费了系统资源,多了造成系统拥挤效率不高。用线程池控制线程数量,其他线程排队等候。一个任务执行完毕,再从队列 的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程池 中有等待的工作线程,就可以开始运行了;否则进入等待队列。

优点:能立即为请求服务,提高了响应速度;可以通过调整线池中线程的数目防止出现资源不足的情况。

缺点:使用线程池构建的应用程序和其他多线程应用程序一样容易遭受并发错误,如同步错误和死锁;还容易遭受特定于线程 池的其他少数风险,如与池有关的死锁,资源不足和线程泄漏、请求过载。

解释一下几个关键词

并发错误:线程池和其他并发机制依靠外套wait()和notify()方法,如果编码不正确,那么可能丢失通知,导致线程保持空 闲状态。

死锁:一般的死锁,满足死锁的四个必要条件;池死锁,线程池中的所以线程都在执行已阻塞的等待队列中另一任务的执行结 果的任务,而这个任务因为没有可以占用的线程而无法运行。

资源不足:如果线程池过大,被线程消耗的资源可能会严重影响系统性能;虽然线程之间切换的调度开销很小,但如果有很多 线程,那么切换可能会严重地影响程序的性能。

线程泄漏:当从池中除去一个线程以执行任务,而任务结束后线程没有返回线程池。当任务抛出一个RuntimeException或一 个Error时,如果池类没有捕获他们,那么线程只会退出线程池大小将永久减一,发生次数过多将导致线程池为空,系统将停 止,没有线程可供使用;有些线程会永远等待某些资源或者用户输入,而这些资源不能保证变得可用,或者用户已经离开,如 果一个线程永久的消耗着,那么相当于从池中除去,应该要么只给予它们自己的线程,要么只让它们等待有限的时间。

请求过载:请求压垮服务器。可以简单地抛弃请求,依靠更高级别的协议稍后重试请求;也可以用服务器暂时很忙的响应来拒 绝请求。

为什么要使用线程池

1.减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。

2.可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大 约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真 正的线程池接口是ExecutorService。

常用线程池

1.newSingleThreadExecutor

创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因 为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

2.newFixedThreadPool

创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值 就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

3.newCachedThreadPool

创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,

那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程 池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

4.newScheduledThreadPool

创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。