HashMap和ConcurrentHashMap和HashTable
- HashMap、HashTable、ConcurrentHashMap区别
答:首先三者都实现了Map接口,存储都是 Key、Value 的键值对。
HashMap是线程不安全的,HashTable、ConcurrentHashMap是线程安全的。
HashMap:存在多个线程同时修改同一数据得情况,所以非线程安全
HashTable:线程安全的。HashTable的每一个方法都加Synchronize锁,直接锁住了方法,方法每次只能被一个线程执行,所以线程安全的,但是造成效率低
ConcurrentHashMap:在HashMap和HashTable之间,做一个平衡,达到线程安全并且锁的粒度更小,锁住的是数组的一个节点
- HashMap和ConcurrentHashMap 1.7和1.8+
1.7采用的是数组+链表数据结构
1.8+采用的是数组+红黑树的数据结构
链表变成红黑树,时间复杂度 从(O)n -> (O)logn,提升查询效率。
当数组长度达到64位,链表长度大于等于8位的时候,才会进行红黑树的转换
- 为什么在元素为8的时候链表变成树,为什么在元素为6的时候从树退回链表
单个TreeNode占用的内存大约是普通Node的两倍,根据时间和空间的角度来看,当节点达到8这个阈值的时候,在进行红黑树的转换。为什么阈值为8呢?如果hash计算的结果离散性好的话,那么节点就会在数组上均匀分布,很少情况下出现链表很长的情况,根据概率统计计算,长度达到8的时候,概率小于千万分之一,所以选择阈值为8
- HashMap的扩容机制
HashMap默认的容量16,负载因子0.75,每次扩容是容量2倍。
扩容的本质,将老的数据,迁移到新的数组中
多线程并发扩容:1.记录扩容的线程数量 2.线程数据迁移完毕后,退出的时候,减少记录数量
- HashMap的容量为什么要设置为2的次幂
个人理解:便利数据的迁移,HashMap中数据高低位迁移。
将老数据通过位运算,会得到 低位链表和高位链表,低位链表挂到新数组原来的位置,高位链表挂到新数组的高位
- 是否看过HashMap的源码?底层实现是怎么样子的?红黑树的特性是什么?怎么实现的?
看过。JDK1.7 数组+链表,JDK1.8 数组+红黑树。一些别的问题,看上方答案
首先:树结构是为了提升整体的效率:插入、删除、查找。平衡树的时间复杂度O(logn) 链表时间复杂度O(n)
红黑树属于高效的自平衡二叉查找树。
通过:颜色变化和旋转操作等,达到平衡效果
红黑树规则:
1. 根节点是黑色
2. 外部节点均为黑色
3. 节点若为红色,则其孩子节点必为黑色
4. 从任一外部节点到根节点的沿途,黑节点的数目相等
- 为什么用红黑树不用普通的AVL树
红黑树相对于AVL树插入的更快
1. AVL树和红黑树都是高度平衡的树数据结构,它们非常相似,真正的区别在于任何添加/删除操作时完成的旋转操作次数
2. AVL树在查找密集型任务上更快,利用更好的平衡,树遍历平均更短。另一方面,插入和删除方面,AVL树速度较慢,需要更高的旋转次数才能在修改时正确的平衡数据结构
3.AVL树种,从根到任何叶子的最短路径和最长路径之间的差异最多为1,在红黑树中,差异可以是2倍
Synchronized、Lock、Volatile
- 锁解决的问题
原子性、可见性、有序性
- Synchronized和Lock的区别
Synchronized是Java关键字,非公平锁,自动释放锁
Lock是接口,公平锁和非公平锁,更加的灵活,但是 需要手动加锁和手动释放锁,锁的Api更加丰富
- 锁的四种状态及升级过程
1. 无锁状态
2. 偏向锁:当有一个线程获取到锁,对象就会升级偏向锁,并且在对象中记录获取到锁的线程
3. 轻量级锁:当锁被别的线程持有的过程中,又来了别的线程竞争锁,那么偏向锁被升级成轻量级锁。来竞争锁的线程处于自旋的状态。这块是一个for循环,循环的去竞争锁,自旋次数是自适应的。当自旋超过自旋次数了,锁膨胀为重量级锁
4. 重量级锁:线程被阻塞,阻塞线程加入到同步队列,等待前驱线程释放锁,唤醒同步队列中的线程去抢占锁。
- 公平锁和非公平锁
公平锁:按照同步队列中插入的先后顺序,去获得锁
非公平锁:不一定按照同步队列插入的先后顺序去获得锁,这个线程只要没进入到同步队列中,只要有机会,就去抢占一次锁
- Synchronized在JDK1.6之后做了哪些优化?
增加了:偏向锁、轻量级锁。当遇到多线程竞争锁的时候,不直接加重量级锁,而是有一个锁升级的过程。从而提升了Synchronized性能
- Synchronized和ReentrantLock的底层实现及重入的底层原理
Synchronized底层实现:
锁的标记存储在对象头中,通过monitorenter和monitorexit指令实现同步代码块,同步代码块针对一个线程是可重新进入的,竞争的线程 被阻塞放在队列中。每个锁,都会记录线程的持有者和计算器,同一个线程进入同步代码块一次计算器+1,执行完毕一次计数器-1。当计数器为0的时候释放锁
ReentrantLock底层实现:
ReentrantLock中实现了Sync,Sync继承自AbstractQueuedSynchronizer,被阻塞的线程,就会被放在AQS同步队列中,这里就体现了 公平锁和非公平锁
通过LockSupport.park()阻塞线程,通过LockSupport.unpark()去唤醒线程
锁中记录线程持有者和state值,当同一个线程重新进入state+1。执行完毕state值-1
- 讲讲独占锁ReentrantLock原理?谈谈读写锁ReentrantReadWriteLock原理?
ReentrantLock中维护了一把锁,执行同步代码块,就需要先获取锁,再去执行。原理在上题中
ReentrantReadWriteLock中维护了两把锁,一把读锁 一把写锁,读锁可以共享,写锁只能是一个线程持有
- Volatile关键字 CAS(比较与交换)实现原理
Volatile解决了 可见性、有序性
解决了缓存一致性问题,禁止指令重排
CAS:Compare And Swap。内存中的值和预期值 做比较,如果相等 将内存值替换为新值。乐观锁概念
- Volatile的可见性和禁止指令重排序怎么实现的
可见性:缓存一致性,每个CPU高速内存中的值,都是一样的,所以每个线程看到的值就是一致的了。怎么解决缓存一致性:总线锁、缓存一致性协议(EMSI)
指令重排:通过内存屏障实现的。系统提供了读屏障、写屏障、全屏障。使得指令按顺序执行
- 说一下EMSI缓存一致性协议具体
MESI中每个缓存行都有四个状态,分别是E(exclusive)、M(modified)、S(shared)、I(invalid)
E:独占,表示该缓存行对应内存中的内容只被该CPU缓存,其他CPU没有缓存该缓存对应内存行的内容
M:修改,表示该缓存行中的内容被修改了,并且该缓存行只被缓存在该CPU中。这个状态缓存行中的数据和内存中的数据不一致,需被写入到内存中
S:分享,表示该缓存行的数据不只在该CPU缓存了,在其他CPU也缓存了。这个状态的数据和内存中的数据时一致的。当有一个CPU修改该缓存行对应的内存的时,会使别的CPU缓存的数据变为I状态
I:表示该缓存行中的内容无效
- 除了CAS,原子类,Sync,Lock还有什么线程安全的方式
Volatile 可以保证可见性、有序性,但是 不能保证原子性
final修饰的变量
Happens-Before模式
- CAS的ABA问题怎么解决?
ABA问题:CAS在检查值得时候,只会比较预期值和内存值是否相同,如果内存值,经过若干次修改又变回了A(A->B->A),CAS检查依旧会通过,但是实际上这个值已经被修改过了
解决方案:引入乐观锁的版本号控制,不止比较预期值和内存位置的值,还要比较版本号是否正确
实现案例:从JDK1.5开始,atomic包就提供了AtomicStampedReference类来解决ABA问题,AtomicStampedReference不仅维护了对象值,还维护了一个时间戳,每次更新对象值得时候也会更新时间戳。只有当对象值和时间戳都相等的时候,在进行更新操作
- JMM共享内存模型以及8个原子操作指令
JMM(Java Memory Model)Java内存模型,就是屏蔽各种硬件和操作系统的访问差异,保证Java程序在各种平台对内存的访问都能保证效果一致的机制和规范。
8个原子指令:
lock(锁定):作用于主内存,它把一个变量标记为一条线程独占状态;
read(读取):作用于主内存,它把变量值从主内存传送到线程的工作内存中,以便随后的load动作使用;
load(载入):作用于工作内存,它把read操作的值放入工作内存中的变量副本中;
use(使用):作用于工作内存,它把工作内存中的值传递给执行引擎,每当虚拟机遇到一个需要使用这个变量的指令时候,将会执行这个动作;
assign(赋值):作用于工作内存,它把从执行引擎获取的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时候,执行该操作;
store(存储):作用于工作内存,它把工作内存中的一个变量传送给主内存中,以备随后的write操作使用;
write(写入):作用于主内存,它把store传送值放到主内存中的变量中。
unlock(解锁):作用于主内存,它将一个处于锁定状态的变量释放出来,释放后的变量才能够被其他线程锁定;
- 什么是Java指令重排序?
在执行程序的过程时为了提升性能,编译器和处理器常常会对指令做重排序
编译器重排:不改变程序执行的结果情况下,可以对语句执行先后进行重排
处理器重排:处理器处理缓存一致性的时候,可能是乱序执行
- 什么情况下会发生线程死锁
同时满足已下4个条件:
1. 互斥条件:一个资源,只能被一个线程持有
2. 不剥夺条件:占有资源的时候,不能被别的线程抢夺
3. 占用并等待:一个线程占有一个资源,并且等待另一个资源
4. 循环等待:A等B,B等A
Wait、Notify
- Wait/Notify体现了什么设计模式
个人理解:没找到恰当的设计模式,消费者/生产者模式
AQS
- AQS原理
AQS(AbstractQueuedSynchronizer),如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用CLH队列的变体实现,将暂时获取不到锁的线程加入到队列中
参考链接:从ReentrantLock的实现看AQS的原理及应用 - 美团技术团队
- AQS的用处
J.U.C中大多数的并发工具都是基于AQS实现线程的阻塞等待唤醒的,例如:ReentrantLock、CountDownLatch、Semaphore等
为什么说AQS的底层是CAS+Volatile
CAS操作能保证线程安全,向阻塞队列中添加元素的时候通过CAS保证线程安全
Volatile保证可见性、有序性,被修饰的元素多线程可见的,例如:state值
JUC包里的同步组件主要实现了AQS的哪些主要方法
tryAcquire 获取锁,公平锁和非公平锁有不同的实现
tryRelease 释放锁,每个实现类有不同的实现
tryAcquireShared,共享锁实现这个方法获取锁
tryReleaseShared,共享锁实现这个方法释放锁
- 阻塞队列的实现方式,为什么会阻塞
个人理解:和AQS原理一样的回答,如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用CLH队列的变体实现,将暂时获取不到锁的线程加入到队列中
- 什么叫做阻塞队列的有界和无界
阻塞队列有一个非常重要的属性,那就是容量的大小,分为有界和无界
无界队列意味着可以容纳没有边界个元素。例如 LinkedBlockingQueue可以容纳Integer.MAX_VALUE,约为2的31次方,是一个非常大的数,接近无界
有界队列意味着容量可以装满,例如:ArrayBlockingQueue初始化的时候需要传入容量的大小,就意味着能容纳有限个元素
- PriorityQueue底层是什么,初始容量是多少,扩容方式呢
PriorityQueue通过二叉小顶堆实现的,优先级最高的放在顶堆,从顶堆取元素,实现优先级高的先出去
默认的初始容量11
扩容:
如果先前容量 < 64,那么新队列长度=原队列长度*2+2
如果先前容量 > 64,那么新队列长度=原队列长度*1.5
- 你还知道什么阻塞队列,能具体说说它们的特点吗
ArrayBlockingQueue基于数组结构
LinkedBlockingQueue基于链表结构
PriorityBlockingQueue基于优先队列
DelayQueue允许延时执行的队列
SynchronousQueue没有任何存储结构的队列
LinkedBlockingDeque 双向链表组成的队列,支持双向插入和移除
- CLH队列怎么存储数据,具体细节
CLH也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,它不断轮训前驱的状态,如果发现前驱释放了锁就结束自旋
- ASQ中的state字段作用
state意为同步状态,是由volatile修饰的,用于展示当前临界资源的获锁情况。
ThreadLocal
- ThreadLocal作为变量的线程隔离方式,其内部是如何做到的?
通过私有的ThreadLocalMap实现线程之间的隔离,每个线程都会创建对应的ThreadLocalMap来存储值,每个Entry的key是ThreadLocal对象的弱引用,value是对应的值
- 为什么Entry的key是弱引用
因为如果ThreadLocal对象被清理掉了,那么Entry的key就会为空,set()get()方法都会帮助清理空节点
- ThreadLocal内存泄漏的问题
* 根据源码,我们知道get()set()方法都可以触发清理方法,所以正常情况下不会出现内存溢出的情况,但是如果没有调用get和set方法的时候就会可能面临内存溢出,养成好的习惯对象不使用的时候调用remove(),加快垃圾回收,避免内存溢出
* 退一步说,就算我们没有get、set和remove方法,线程结束的时候,也就没有强引用在指向ThreadLocalMap,这样ThreadLocalMap和里面的元素都会被清理掉,但是,如果是线程池,就会存储内存泄漏的问题
- ThreadLocal源码,怎么解决哈希冲突?
通过线性探索的方式,解决hash冲突
解决Hash冲突的4种方法
* 开放寻址法(线性探索)
* 再hash方法
* 链地址法(HashMap就是这么做的)
* 建一个公共溢出区
并发工具
- CountDownLatch内部实现与CyclicBarrier有何不同?
CountDownLatch:计算器的功能,等待其他线程执行完毕之后,主线程在执行
CyclicBarrier:循环栅栏的功能,多个线程同时阻塞,等待达到某一个临界值的时候,同时执行
CountDownLatch的同步功能是基于AQS实现的,CountDownLatch使用AQS中的state成员变量作为计算器,在state不为0的时候,凡是调用await方法的线程都会被阻塞,并放入AQS所维护的阻塞队列中进行等待,当state被减至0的时候,队列中的线程按顺序被唤醒
CyclicBarrier并没有直接通过AQS实现同步功能,而是在ReentrantLock的基础上实现的。在CyclicBarrier中,线程访问await方法需先获取锁才能访问。在最后一个线程访问await方法前,其他线程进入await方法后,都会被Condition的await方法进入等待状态。当最后一个线程进入,该线程会调用Condition的singleAll方法唤醒所有处于等待的线程。同时最后一个线程会重置CyclicBarrier,使其重复使用
- JUC包里的限流该怎么做到
Semaphore限流器,控制资源的访问
本质上:抢占令牌,如果抢占到令牌,就通行,否则,就阻塞
- CountDownLatch与线程join方法的区别是什么?
join主要是为了让一个或者多个线程优先于某个线程执行完毕
CountDownLatch主要是内部程序计数器到0之前某一个或者多个线程等待
join和CountDownLatch谈不上区别,只是可以用CountDownLatch模拟join所需要的结果
- Semaphore的内部实现是怎样的?
Semaphore用于管理信号量,Semaphore在实例化的时会传入一个int值,也就是指明信号数量。主要方法有两个:acquire()和release()。acquire用于请求一个信号,每调用一次,信号量便少一个。release()用于释放信号,调用一次信号量加一个。信号量用完以后,后续使用acquire()方法请求信号的线程会加入到阻塞队列挂起
线程、线程池
- 线程有几种实现方式,它们之间的区别是什么?
继承Thread类,类 无返回值
实现Runnable接口,接口 无返回值
实现Callable接口,接口 有返回值
线程池中获取,可重复使用
- ThreadPoolExecutor原理,线程池实现原理?
1. 先初始化核心线程
2. 调用阻塞队列的方法,把task存进去(offer -> true/false)
a.如果是true,说明当前的请求量不大,核心线程数就可以搞定
b.如果是false,增加工作线程数(非核心线程数)
i.如果添加失败,说明当前的工作线程数量达到最大的线程数。直接调用拒绝策略
3.通过CAS来增加核心线程数
4.如果没有任务的时候,核心线程数就会被阻塞住
5.如果当前工作线程大于核心线程数,并且超时没有任务,那么就会CAS减少工作线程数,超时的实现是通过阻塞队列的超时方法workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) 实现的
- 创建线程池的参数是什么?
public ThreadPoolExecutor(
int corePoolSize, //核心线程数
int maximumPoolSize, //最大线程数
long keepAliveTime, //存活时间
TimeUnit unit, //存活单位
BlockingQueue<Runnable> workQueue, //阻塞队列
ThreadFactory threadFactory, //线程工厂,用来创建工作线程的。 默认实现(自定义线程池中线程的名字)
RejectedExecutionHandler handler)//拒绝执行策略 。默认实现
- 线程池的设计里体现了什么设计模式?
享元模式:将线程都放在阻塞队列中存起来了
策略模式:JDK提供几个常用的拒绝策略
装饰器模式:Executors对ThreadPoolExecutor进行了装饰
- Executors创建线程池的方式?
* newFixedThreadPool固定线程数量
* newSingleThreadExecutor只有一个线程的线程池
* newCachedThreadPool 可以缓存的线程池,理论上来说,有多少请求,该线程池就可以创建多少的线程池
* newScheduleThreadPool 提供了按照周期执行的线程池
- CachedThreadPool里面用的什么阻塞队列?
SynchronousQueue没有任何存储结构的队列
- 线程池的线程数怎么设置比较好?
* IO密集型 2core+1 。(CPU利用率不高)
* CPU密集型 core+1 。(CPU利用率很高,会增加上下文切换)
应用
- 有3个线程,线程A和线程B并行执行,线程C需要A和B执行完后才能执行。可以怎么实现?
方案一:CountDownLatch+Semaphore
方案二:CyclicBarrier