本文整理了牛客上一个很棒的Java并发编程的八股,供童鞋们参考学习~
ConcurrentHashMap
什么是ConcurrentHashMap?相比于HashMap和HashTable的优势?
ConcurrentHashMap可以看成是线程安全且高效的HashMap,它比HashMap更安全,比HashTable更高效。
Java中的ConcurrentHashMap是如何实现的?
- JDK1.7
在JDK1.7中,ConcurrentHashMap是由segment分段锁数组构成,每个segment数组由多个HashEntry链表数组构成,因此ConcurrentHashMap定位一个元素需要两次hash,第一次hash的目的是定位到segment,第二次hash的目的是定位到链表的头部。
- JDK1.8
JDK1.8之后的ConcurrentHashMap结构与HashMap相似,数组+链表/红黑树,当链表节点个数大于8时,链表会转换为红黑树。
从上图可以看出,JDK1.8的实现方式可以降低锁粒度。
ConcurrentHashMap结构变量中使用volatile和final修饰有什么作用?
final修饰变量可以保证变量不需要同步就可以被访问和共享,volatile可以保证内存的可见性,同时配合CAS操作可以在不加锁的情况下实现并发。
ConcurrentHashMap默认初始容量,扩容机制?
默认初始容量为16,之后每次扩容为之前的2倍。
ConcurrentHashMap的key和value是否可以为null?HashMap呢?
- HashMap的key和value都可以为null,但key作为null只能有一个,value作为null可以有多个;
- ConcurrentHashMap的key和value不能为null,否则会出现空指针异常(HashTable也是)。
原因:
ConcurrentHashMap是在多线程场景下使用的,如果ConcurrentHashMap.get(key)的值为null,无法辨别是key对应的value值为null,还是根本不存在这样的key值;而在单线程下使用的HashMap,可以使用containsKey(key)来判断到底是不存在key还是key对应的value值是null。
ConcurrentHashMap在JDK1.7和JDK1.8版本中的区别?
- 结构上的不同
JDK1.7是segment分段锁数组,JDK1.8是数组+链表/红黑树。 - 保证线程安全方面
JDK1.7采用分段锁机制,当一个线程占用锁时,一部分数据,提高了并发访问率;
JDK1.8利用synchronized和CAS的方式保证线程安全。 - 存取数据方面
- JDK1.7中的put()方法
(1)先计算key对应的hash值,利用hash值对segment数组取余得到对应的segment对象;
(2)尝试获取锁,如果失败则自旋直至成功,获取到锁后,利用hash值对HashEntry数组进行取余得到对应的entry对象;
(3)遍历链表,查找对应的key值,如果找到则将旧的value值覆盖;否则添加到链表中。(1.7是头插,1.8是尾插) - JDK1.8中的put()方法
(1)计算key对应的hash值,利用hash值找到对应的数组下标,如果当前位置为空就直接写入数据;
(2)利用CAS尝试写入,如果失败则自旋直至成功,如果都不满足,则利用synchronized锁写入数据。
ConcurrentHashMap迭代器的弱一致性
与HashMap不同的是,ConcurrentHashMap迭代器是弱一致性。
当ConcurrentHashMap的迭代器创建以后,在遍历hash表元素的过程中,hash表的中的元素可能会发生变化,如果这部分变化发生在已经遍历过的地方,迭代器不会反映出来,如果这部分变化发生在未遍历的地方,迭代器则会反映出来。换种说法就是put()方法将一个元素插入到底层数据结构后,get()可能在某段时间内害看不到这个元素。
这样的设计主要是为了ConcurrentHashMap的性能考虑,如果想做到强一致性,需要到处加锁,这样性能会下降很多。
ThreadLocal
什么是ThreadLocal?有哪些应用场景?
ThreadLocal是JDK java.lang包下的一个类,ThreadLocal为变量在每个线程中都创建一个副本,这样每个线程可以访问自己内部的副本变量,而不会和其它线程的局部变量冲突,实现了线程间的数据隔离。
ThreadLocal的应用场景:
- 保存线程上下文信息,在需要的地方可以获取
- 实现线程间的数据隔离
- 数据库连接
ThreadLocal原理和内存泄漏
每个线程都有一个ThreadLocalMap,而ThreadLocalMap中保存着所有的ThreadLocal,其中ThreadLocal为ThreadLocalMap中的key。
- 为什么ThreadLocal会发生内存泄漏呢?
因为,ThreadLocal中的key是弱引用,而value是强引用。在进行垃圾回收时,key会被清理掉,而value不会被清理掉,这时如果不做任何处理,value将永远不会被回收,产生内存泄漏。 - 如何解决ThreadLocal的内存泄漏?
在使用完ThreadLocal后,调用remove()方法手动清理掉key为null的记录
线程池
什么是线程池以及为何要使用线程池?
线程池是一种实现多线程的方式。在线程池中提前创建好多个线程,使用时直接获取,使用完放回池中。
- 为什么要使用线程池?
- 降低资源消耗:通过重复利用已经创建的线程,避免了创建、销毁线程带来的开销;
- 提高响应速度:当任务到达时,不需要创建线程,可以立马执行;
- 提高线程的可管理性:由线程池统一分配调度。
创建线程池的几种方法
- Executors工厂方法提供了以下几个常见的静态的工厂方法:
newSingleThreadExecutor:创建一个单线程的线程池
newFixedThreadPool:创建固定大小的线程池
newCachedThreadPool:创建可缓存的线程池
newScheduledThreadPool:创建一个无限大小的线程池
- new ThreadPoolExecutor方法
ThreadPoolExecutor构造函数的重要参数解析
三个重要参数:
corePoolSize:核心线程数,定义了最小可以同时运行的线程数量
maximumPoolSize:线程中允许存在的最大工作线程数量
workQueue:阻塞队列。新来的任务会先判断当前运行的线程数是否达到了核心线程数,如果达到的话,任务就会先放到阻塞队列。
线程池的执行流程
ThreadPoolExecutor的饱和策略(拒绝策略)
当阻塞队列已满并且同时运行的线程数量达到了线程池的容量时,就会执行饱和策略,主要有以下四种类型:
- AbortPolicy策略:直接抛出异常拒绝新任务
- CallerRunsPolicy策略:当线程池无法处理当前任务时,会将该任务交由提交任务的线程来执行
- DiscardPolicy策略:直接丢弃新任务
- DiscardOlddestPolicy:丢弃最早的未处理的任务请求
execute()方法和submit()方法的区别
- execute()方法只能执行Runnable类型的任务;submit()方法可以执行Runnable和Callable类型的任务。
- submit()方法可以返回持有计算结果的Future对象,同时可以跑出异常;execute()方法不可以。