并发面试题

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值