并发编程相关面试题(持续更新)

1.juc包提供了哪些工具?用在什么场景?

1.提供了比 synchronized 更加高级的各种同步结构,包括 CountDownLatchCyclicBarrierSemaphore 等
场景:可以实现更加丰富的多线程操作,比如利用 Semaphore 作为资源控制器,限制同时进行工作的线程数量。

2.各种线程安全的容器,比如最常见的 ConcurrentHashMap、有序的 ConcunrrentSkipListMap,或者通过类似快照机制,实现线程安全的动态数组 CopyOnWriteArrayList 等。

3.各种并发队列实现,如各种 BlockedQueue 实现,比较典型的 ArrayBlockingQueueSynchorousQueue 或针对特定场景的 PriorityBlockingQueue 等。

4.强大的 Executor 框架,可以创建各种不同类型的线程池,调度任务运行等,绝大部分情况下,不再需要自己从头实现线程池和任务调度器。

2.线程池有哪些定制参数?分别有什么作用?拒绝策略有哪些?哪种拒绝策略好?

'主要参数和作用'
1.corePoolSize
所谓的核心线程数,可以大致理解为长期驻留的线程数目(除非设置了 allowCoreThreadTimeOut)。对于不同的线程池,这个值可能会有很大区别,比如 newFixedThreadPool 会将其设置为 nThreads,而对于 newCachedThreadPool 则是为 02.maximumPoolSize,就是线程不够时能够创建的最大线程数。同样,对于 newFixedThreadPool,当然就是 nThreads,因为其要求是固定大小,而 newCachedThreadPool 则是 Integer.MAX_VALUE。

3.keepAliveTime 和 TimeUnit,这两个参数指定了额外的线程能够闲置多久。
如果一个线程空闲了keepAliveTime & TimeUnit这么久,而且线程池的线程数大于 corePoolSize ,那么这个空闲的线程就要被回收了。 

4.workQueue,工作队列,必须是 BlockingQueue5.threadFactory:通过这个参数你可以自定义如何创建线程,例如你可以给线程指定一个有意义的名字。

6.handler:通过这个参数你可以自定义任务的拒绝策略。如果线程池中所有的线程都在忙碌,并且工作队列也满了(前提是工作队列是有界队列),那么此时提交任务,线程池就会拒绝接收。至于拒绝的策略,可以通过 handler 这个参数来指定。


'4种拒接策略'1.CallerRunsPolicy:提交任务的线程自己去执行该任务。
2.AbortPolicy:默认的拒绝策略,会 throws RejectedExecutionException3.DiscardPolicy:直接丢弃任务,没有任何异常抛出。
4.DiscardOldestPolicy:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列。

Java1.6 版本还增加了 allowCoreThreadTimeOut(boolean value) 方法,它可以让所有线程都支持超时,这意味着如果项目很闲,就会将项目组的成员都撤走

'拒绝策略的选取':
具体要看执行的任务重要程度。如果是一些不重要任务,可以选择直接丢弃。
但是如果为重要任务,可以采用降级处理,例如将任务信息插入数据库或者消息队列,启用一个专门用作补偿的线程池去进行补偿

3.Executors提供了哪些类型的线程池?分别用于什么场景?使用线程池要注意些什么

Executors 目前提供了 5 种不同的线程池创建配置:
1.newCachedThreadPool():它是一种用来处理大量短时间工作任务的线程池,具有几个鲜明特点
(1)它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程
(2)如果线程闲置的时间超过 60 秒,则被终止并移出缓存;
(3)时间闲置时,这种线程池,不会消耗什么资源。其内部使用 SynchronousQueue 作为工作队列。

2.newFixedThreadPool(int nThreads):重用指定数目(nThreads)的线程,其背后使用的是'无界的工作队列',任何时候最多有 nThreads 个工作线程是活动的
(1)如果任务数量超过了活动队列数目,将在工作队列中等待空闲线程出现
(2)如果有工作线程退出,将会有新的工作线程被创建,以补足指定的数目 nThreads。

3.newSingleThreadExecutor():
特点是工作线程数目被限制为 1,操作一个无界的工作队列,所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态,并且不允许使用者改动线程池实例,因此可以避免其改变线程数目。

4.newSingleThreadScheduledExecutor()newScheduledThreadPool(int corePoolSize)
创建的是个 ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程。

5.newWorkStealingPool(int parallelism)
这是一个经常被人忽略的线程池,Java 8 才加入这个创建方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序。

'不建议使用 Executors 的最重要的原因是':
1.Executors 提供的很多方法默认使用的都是无界的 LinkedBlockingQueue,高负载情境下,无界队列很容易导致 OOM,而 OOM 会导致所有请求都无法处理,这是致命问题。所以强烈建议使用有界队列。
2.使用有界队列,当任务过多时,线程池会触发执行拒绝策略,线程池默认的拒绝策略会 throw RejectedExecutionException 这是个运行时异常,对于运行时异常编译器并不强制 catch 它,所以开发人员很容易忽略。因此默认拒绝策略要慎重使用
3.如果线程池处理的任务非常重要,建议自定义自己的拒绝策略;并且在实际工作中,自定义的拒绝策略往往和降级策略配合使用。
4.使用线程池,还要注意异常处理的问题,例如通过 ThreadPoolExecutor 对象的 execute() 方法提交任务时,如果任务在执行的过程中出现运行时异常,会导致执行任务的线程终止;不过,最致命的是任务虽然异常了,但是你却获取不到任何通知,这会让你误以为任务都执行得很正常。最稳妥和简单的方案还是捕获所有异常并按需处理
try {
  // 业务逻辑
} catch (RuntimeException x) {
  // 按需处理
} catch (Throwable x) {
  // 按需处理
} 

4.AQS提供了哪些扩展点?

1.AQS是什么?有什么用?
AQS全称 AbstractQueuedSynchronizer ,即抽象的队列同步器,是一种用来构建锁和同步器的框架。
基于AQS构建同步器:
- ReentrantLock
- Semaphore
- CountDownLatch
- ReentrantReadWriteLock
- SynchronusQueue
- FutureTask
优势:
1.AQS 解决了在实现同步器时涉及的大量细节问题,例如自定义标准同步状态、FIFO 同步队列。
2.基于 AQS 来构建同步器可以带来很多好处。它不仅能够极大地减少实现工作,而且也不必处理在
多个位置上发生的竞争问题。


2.主要是提供自定义同步器的实现:
在构建自定义同步器时,只需要依赖AQS底层再实现**共享资源state的获取与释放操作**即可。
自定义同步器实现时主要实现以下几种方法:

(1)isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。

(2)tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false(3)tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false(4)tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。

(5)tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false

5.AQS实现原理大概是怎么样的?

1.AQS:AbstractQuenedSynchronizer抽象的队列式同步器。是除了java自带的synchronized关键字之外的锁机制。
2.AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包

3.AQS的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

CLH(CraigLandin,and Hagersten)队列是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系。
'AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node),来实现锁的分配。'
用大白话来说,'AQS就是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒'

4.AQS实现的具体方式如下:
如下图示,AQS维护了一个volatile int state和一个FIFO线程等待队列,多线程争用资源被阻塞的时候就会进入这个队列。
state就是共享资源,其访问方式有如下三种:
(1getState();2setState();3compareAndSetState();

AQS 定义了两种资源共享方式:
1.Exclusive:独占,只有一个线程能执行,如ReentrantLock
2.Share:共享,多个线程可以同时执行,如SemaphoreCountDownLatchReadWriteLockCyclicBarrier

不同的自定义的同步器争用共享资源的方式也不同

在这里插入图片描述

6.ReentrantLock和synchronized的区别?synchronized锁升级过程?

'区别:'
1.synchronized 是JVM实现的一种锁,其中锁的获取和释放分别是monitorenter 和 monitorexit 指令。
它提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其他试图获取的线程只能等待或者阻塞在那里。在 Java 5 以前,synchronized 是仅有的同步手段,在代码中, synchronized 可以用来修饰方法,也可以使用在特定的代码块儿上,本质上 synchronized 方法等同于把方法全部语句用synchronized 块包起来。

2.ReentrantLock,通常翻译为再入锁,是 Java 5 提供的锁实现,它的语义和 synchronized 基本相同,再入锁通过代码直接调用 lock() 方法获取,代码书写也更加灵活。与此同时,ReentrantLock 提供了很多实用的方法,能够实现很多 synchronized 无法做到的细节控制,比如可以控制 fairness,也就是公平性,或者利用定义条件等。但是,编码中也需要注意,必须要明确调用 unlock() 方法释放,不然就会一直持有该锁。

区别总结:
1.ReentrantLock支持等待可中断,可以中断等待中的线程
2.ReentrantLock可实现公平锁
3.ReentrantLock可实现选择性通知,即可以有多个Condition队列

'锁升级过程':
当没有竞争出现时,默认会使用偏斜锁。JVM 会利用 CAS 操作(compare and swap),在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。这样做的假设是基于在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。

如果有另外的线程试图锁定某个已经被偏斜过的对象,JVM 就需要撤销(revoke)偏斜锁,并切换到轻量级锁实现。轻量级锁依赖 CAS 操作 Mark Word 来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁。

7.ReentrantLock如何实现synchronized不具备的公平与非公平性呢

1.lock公平锁模式实现:
在 tryAcquire() 的实现中使用了 hasQueuedPredecessors() 保证了线程先进先出FIFO的使用锁,不会产生"饥饿"问题,
tryAcquire都会检查CLH队列中是否仍有前驱的元素,如果仍然有那么继续等待,通过这种方式来保证先来先服务的原则

在这里插入图片描述
在这里插入图片描述

2.'非公平锁获取锁获取区别主要在于:'
	1).构建函数中传入 false 或者为null,为创建非公平锁 NonfairSync , true 创建公平锁,
	2).非公平锁在获取锁的时候,先去检查 state 状态,再直接执行 aqcuire(1) ,这样可以提高效率,
final void lock() { 
if (compareAndSetState(0, 1)) 
//修改同步状态的值成功的话,设置当前线程为独占的线程 
setExclusiveOwnerThread(Thread.currentThread());
 else
 //获取锁 acquire(1); 
 }
	3).tryAcquire() 中没有 hasQueuedPredecessors() 保证了不论是新的线程还是已经排队的线程都顺序使用锁。

3.ReentrantLock使用场景
场景1:如果已加锁,则不再重复加锁,多用于进行非重要任务防止重复执行,如,清除无用临时文件,检查某些资源的可用性,数据备份操作等
场景2:如果发现该操作已经在执行,则尝试等待一段时间,等待超时则不执行,防止由于资源处理不当长时间占用导致死锁情况
场景3:如果发现该操作已经加锁,则等待一个一个加锁,主要用于对资源的争抢(如:文件操作,同步消息发送,有状态的操作等)
场景4:可中断锁,取消正在同步运行的操作,来防止不正常操作长时间占用造成的阻塞

8.ThreadLocal实现原理是怎么样的?使用时需要注意什么?底层实现的map和HashMap有什么不一样?

1.ThreadLocal是什么、有什么、能做什么?

ThreadLocal提供一个线程(Thread)局部变量,访问到某个变量的每一个线程都拥有自己的局部变量。说白了,ThreadLocal就是想在多线程环境下去保证成员变量的安全。

3.ThreadLocal实现原理

ThreadLocal 和线程的关系如下图所示:

在这里插入图片描述

每个线程内部有个 threadLocalMap,map ⾥⾯存储的 key 是 threadLocal 对象。
这样调 ⽤ threadLocal.get就可以根据当前线程找到本地的 map,
然后根据调⽤的 threadLocal 对象找到对应 的 value。

在这里插入图片描述
在实现上 threadLocalMap 是⼀个由 Entry 对象组成的数组,Entry 对 key 的引⽤为弱引⽤,对 value 的引⽤为强引⽤。
在这里插入图片描述
整体相关的对象引⽤链如下:
在这里插入图片描述

3.引发的问题:内存泄漏

基于上⾯的图,我们来看看谈到 threadLocal 经常说的内存泄漏指的是什么?
1.由于线程池内线程⽣命周期较⻓,所以图中下⽅的那条强引⽤链会⼀直存在,⽽图上⽅的强引⽤链随 着⽅法的调⽤结束出栈之后就不复存在了,所以当前的 threadLocal 对象只有⼀条弱引⽤存在(key的弱引⽤)。

2.如果发⽣ gc ,在内存不⾜的时候 threadLocal 对象就会被回收,这样就会残留⽆⽤的 Entry 在线程 对象中(key都没了,根本访问不到 value,所以⽆⽤)。这就是所说的内存泄漏(残留了⽆⽤的 Entry ⽆法回收)。

那既然会有内存泄漏为什么还这样实现?

1.就是因为 key 对 threadLocal 对象之间是弱引⽤,这样在栈上没有 threadLocal 引⽤这个强引⽤之后 (你可以认为之后不会在⽤这个 threadlocal 对象),threadLocal 对象才得以被回收。
2.如果 key 对 threadLocal 对象之间是强引⽤,那么⽆⽤的 threadLocal 对象就⽆法被回收了,这其实 造成了更⼤的内存泄漏。
设计者知晓会出现这种情况,所以在多个地⽅都做了清理⽆⽤ Entry 的操作。

⽐如通过 key 查找 Entry 的时候,如果下标⽆法直接命中,那么就会向后遍历数组,此时遇到 key 为 null 的 Entry 就会清理掉,还有扩容的时候,等等。

本质上是⼀种兜底⽅案,最佳实践是在⽤完之后就⼿动把它 remove 掉,这样就避免了内存泄漏的存在。

4.底层实现的map和HashMap有什么不一样?

ThreadLocalMapHashMap 最大的区别在于解决 hash 冲突
HashMap 使用的拉链法,而 ThreadLocalMap 使用的线性探测法

9.讲述一下并发编程中的原子性,可见性,有序性?

1.原子性
我们把一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性

如果把一个事务可看作是一个程序,它要么完整的被执行,要么完全不执行。这种特性就叫原子性。

2.可见性
一个线程对共享变量的修改,另外一个线程能够立刻看到

3.有序性
有序性指的是程序按照代码的先后顺序执行。
编译器为了优化性能,有时候会改变程序中语句的先后顺序。
例如程序中:“a=6;b=7;”编译器优化后可能变成“b=7;a=64.如何解决可见性和有序性的问题
"按需禁用缓存以及编译优化"
所谓“按需禁用”其实就是指按照程序员的要求来禁用。
所以,为了解决可见性和有序性问题,只需要提供给程序员"按需禁用缓存和编译优化的方法"即可。
这些方法包括 volatilesynchronizedfinal 三个关键字,以及六项 Happens-Before 规则

10.volatile,jmm,mesi三者怎么理解?*

1.volatile

我们声明一个 volatile 变量 volatile int x = 0,它表达的是:
告诉编译器,对这个变量的读写,不能使用 CPU 缓存,必须从内存(公共内存)中读取或者写入

即一个线程修改了主物理内存的值,主物理内存的值被修改,其他线程马上获得通知

2.JMM

Java Memory Model(Java内存模型),围绕着在并发过程中如何处理可见性、原子性、有序性这三个特性而建立的模型。

3.MESI:缓存一致性协议

因为内存和cpu速率不一致,所以有了cpu高速缓存

因为摩尔定律,从而发展多核cpu,出现缓存不一致问题,所以引入mesi

由于mesi依赖总线,为了优化性能,引入store buffer和Store Forwarding和invalid queue,但是产生了新的一致性问题(重排序现象,这里只是重排序的原因之一)

为了解决新的一致性问题,引入了内存屏障,各家cpu厂商都有自己的方案,指令都不同
jvm为了屏蔽以上细节,从而定义了jmm,抽象出工作内存等概念,也抽象自己一套内存屏障指令

volatile就是在jmm之上,体现了可见性和防重排的语义

注意:volatile无法保证原子性

解决volatile不保证原子性问题:

1.方法前加synchronized解决
public synchronized void addPlusPlus() {
 number++; 
 }

2.加锁解决
Lock lock = new ReentrantLock(); 
public void addPlusPlus() { 
lock.lock();
number++;
lock.unlock();

3.原子类解决
 }

11.什么是jmm?八大原子操作是哪些?*

JMM:**Java Memory Model(Java内存模型)**
八大原子操作是:
lock(锁定)   unlock(解锁)  read(读取)  load(载入)
use(使用)  assign(赋值)  store(存储)  write(写入)

12.什么是mesi?

多核CPU的情况下有多个一级缓存,如何保证缓存内部数据的一致,不让系统数据混乱。
这里就引出了一个"一致性的协议MESI"

MESI协议缓存状态
缓存行(Cache line):缓存存储数据的单元

MESI 是指4中状态的首字母。每个Cache line有4个状态,可用2个bit表示,它们分别是:
在这里插入图片描述
注意:
对于M和E状态而言总是精确的,他们在和该缓存行的真正状态是一致的,而S状态可能是非一致的。

如果一个缓存将处于S状态的缓存行作废了,而另一个缓存实际上可能已经独享了该缓存行,但是该缓存却不会将该缓存行升迁为E状态,这是因为其它缓存不会广播他们作废掉该缓存行的通知,同样由于缓存并没有保存该缓存行的copy的数量,因此(即使有这种通知)也没有办法确定自己是否已经独享了该缓存行。

从上面的意义看来E状态是一种投机性的优化:如果一个CPU想修改一个处于S状态的缓存行,总线事务需要将所有该缓存行的copy变成invalid状态,而修改E状态的缓存不需要使用总线事务。

14.编程题:实现两个线程交替打印,第一个打印0,第二个打印1,第一个打印3,以此类推

在这里插入代码片

15.编程题:用数组实现阻塞队列,考虑线程安全,可以使用juc的并发工具

在这里插入代码片

16.说一下线程安全ConcurrentHashMap的工作原理,java7和8实现上的区别。get()需要加锁吗?size()呢?为什么?

1.JDK1.7版本的CurrentHashMap的实现原理

在JDK1.7中ConcurrentHashMap采用了数组+Segment+分段锁的方式实现:

1.Segment(分段锁)
ConcurrentHashMap中的分段锁称为Segment,它是类似于HashMap的结构。
即内部拥有一个Entry数组,数组中的每个元素又是一个链表,同时又是一个ReentrantLockSegment继承了ReentrantLock)

2.内部结构
从x下面的结构我们可以了解到,ConcurrentHashMap定位一个元素的过程需要进行两次Hash操作。

第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头

在这里插入图片描述

1.优势
写操作的时候可以只对元素所在的Segment进行加锁即可,不会影响到其他的Segment。
这样,在最理想的情况下,ConcurrentHashMap可以最高同时支持Segment数量大小的写操作(刚好这些写操作都非常平均地分布在所有的Segment上)。
"所以,通过这一种结构,ConcurrentHashMap的并发能力可以大大的提高"2.劣势
这一种结构的带来的副作用是Hash的过程要比普通的HashMap要长

2.JDK1.8版本的CurrentHashMap的实现原理

JDK8中ConcurrentHashMap参考了JDK8 HashMap的实现,采用了数组+链表+红黑树的实现方式来设计,内部大量采用CAS操作。

"JDK8中彻底放弃了Segment转而采用的是Node,其设计思想也不再是JDK1.7中的分段锁思想。"
Node:保存key,value及key的hash值的数据结构。其中value和next都用volatile修饰,保证并发的可见性。

Java8 ConcurrentHashMap结构基本上和Java8HashMap一样,不过保证线程安全性

JDK8中ConcurrentHashMap在链表的长度大于某个阈值的时候会将链表转换成红黑树进一步提高其查找性能

总结:

JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurrentHashMap只是增加了同步的操作来控制并发,从JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+红黑树

  1. 数据结构:取消了Segment分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。
  2. 保证线程安全机制:JDK1.7采用segment的分段锁机制实现线程安全,其中segment继承自ReentrantLock。JDK1.8采用CAS+Synchronized保证线程安全。
  3. 锁的粒度:原来是对需要进行数据操作的Segment加锁,现调整为对每个数组元素加锁(Node)
  4. 链表转化为红黑树:定位结点的hash算法简化会带来弊端,Hash冲突加剧,因此在链表节点数量大于8时,会将链表转化为红黑树进行存储。
  5. 查询时间复杂度:从原来的遍历链表O(n),变成遍历红黑树O(logN)。
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

酆都小菜鬼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值