写点什么

JUC 面试题

作者:andy
  • 2022-10-28
    北京
  • 本文字数:8868 字

    阅读完需:约 29 分钟

一、乐观锁和悲观锁的理解及如何实现,有哪些实现方式?

1.乐观锁,每次操作时不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止

2.悲观锁是会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。

3.乐观锁可以使用 volatile+CAS 原语实现,带参数版本来避免 ABA 问题,在读取和替换的时候进行判定版本是否一致

4.悲观锁可以使用 synchronize 的以及 Lock

二、什么是可重入锁(ReentrantLock)?

重入锁指的是在某一个线程中可以多次获得同一把锁,在线程中多次操作有锁的方法

三、reentrantlock 的优点和缺点

reentrantlock 的优点

1.可以添加多个检控条件, 如果使用 synchronized,则只能使用一个. 使用 reentrant locks 可以有多个 wait()/notify() 队列. [译注:直接多 new 几个 ReentrantLock 就可以了,不同的场景/条件用不同的 ReentrantLock ]

2.可以控制线程得到锁的顺序,也就是有公平锁(按照进入顺序得到资源),也可以不按照顺就像.synchronized 一样.

3.可以查看锁的状态, 锁是否被锁上了.

4.可以查看当前有多少线程再等待锁.

reentrantlock 的缺点

1.需要使用 import 引入相关的 Class

2.不能忘记在 finally 模块释放锁,这个看起来比 synchronized 丑陋

3.synchronized 可以放在方法的定义里面, 而 reentrantlock 只能放在块里面. 比较起来, synchronized 可以减少嵌套

四、我们面对 ReentrantLock 和 synchronized 改如何选择?

1.Synchronized 相比 Lock,为许多开发人员所熟悉,并且简洁紧凑,如果现有程序已经使用了内置锁,那么尽量保持代码风格统一,尽量不引入 Lock,避免两种机制混用,容易令人困惑,也容易发生错误。

2.在 Synchronized 无法满足需求的情况下,Lock 可以作为一种高级工具,这些功能包括“可定时的、可轮询的与可中断的锁获取操作,公平队列,以及非块结构的锁”否则还是优先使用 Synchronized。

3.最后,未来更可能提升 Synchronized 而不是 Lock 的性能,因为 Synchronized 是 JVM 的内置属性,他能执行一些优化,例如对线程封闭的锁对象的锁消除优化,通过增加锁的粒度来消除内置锁的同步,而如果基于类库的锁来实现这些功能,则可能性不大。

五、synchronized 和 java.util.concurrent.locks.Lock 的异同?

1.Lock 和 synchronized 有一点明显的区别 —— lock 必须在 finally 块中释放。否则,如果受保护的代码将抛出异常,锁就有可能永远得不到释放!这一点区别看起来可能没什么,但是实际上,它极为重要。忘记在 finally 块中释放锁,可能会在程序中留下一个定时炸弹,当有一天炸弹爆炸时,您要花费很大力气才有找到源头在哪。而使用同步,JVM 将确保锁会获得自动释放。

2.一个 Lock 对象和一个 synchronized 代码块之间的主要不同点是:

synchronized 代码块不能够保证进入访问等待的线程的先后顺序。

你不能够传递任何参数给一个 synchronized 代码块的入口。因此,对于 synchronized 代码块的访问等待设置超时时间是不可能的事情。

synchronized 块必须被完整地包含在单个方法里。而一个 Lock 对象可以把它的 lock() 和 unlock() 方法的调用放在不同的方法里。

六、SynchronizedMap 和 ConcurrentHashMap(效率高)有什么区别?

1.java5 中新增了 ConcurrentMap 接口和它的一个实现类 ConcurrentHashMap。ConcurrentHashMap 提供了和 Hashtable 以及 SynchronizedMap 中所不同的锁机制。比起 synchronizedMap 来,它提供了好得多的并发性。多个读操作几乎总可以并发地执行,同时进行的读和写操作通常也能并发地执行,而同时进行的写操作仍然可以不时地并发进行(相关的类也提供了类似的多个读线程的并发性,但是,只允许有一个活动的写线程)。Hashtable 中采用的锁机制是一次锁住整个 hash 表,从而同一时刻只能由一个线程对其进行操作;而 ConcurrentHashMap 中则是一次锁住一个桶。ConcurrentHashMap 默认将 hash 表分为 16 个桶,诸如 get,put,remove 等常用操作只锁当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有 16 个写线程执行,并发性能的提升是显而易见的。前面说到的 16 个线程指的是写线程,而读操作大部分时候都不需要用到锁。只有在 size 等操作时才需要锁住整个 hash 表。

2.在迭代方面,ConcurrentHashMap 使用了一种不同的迭代方式。在这种迭代方式中,当 iterator 被创建后集合再发生改变就不再是抛出 ConcurrentModificationException,取而代之的是在改变时 new 新的数据从而不影响原有的数据 ,iterator 完成后再将头指针替换为新的数据 ,这样 iterator 线程可以使用原来老的数据,而写线程也可以并发的完成改变。

ConcurrentHashMap 把实际 map 划分成若干部分来实现它的可扩展性和线程安全。这种划分是使用并发度获得的,它是 ConcurrentHashMap 类构造函数的一个可选参数,默认值为 16,这样在多线程情况下就能避免争用。另外一个不同点是,在被遍历的时候,即使是 ConcurrentHashMap 被改动,它也不会抛 ConcurrentModificationException。尽管 Iterator 的设计不是为多个线程的同时使用。

七、CopyOnWriteArrayList 可以用于什么应用场景?

多读少写的场景。读写并不是在同一个对象上。在写时会大面积复制数组,所以写的性能差,在写完成后将读的引用改为执行写的对象。CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出 ConcurrentModificationException。在 CopyOnWriteArrayList 中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。

八、.如何让一段程序并发的执行,并最终汇总结果?

使用 CyclicBarrier 和 CountDownLatch 都可以,使用 CyclicBarrier 在多个关口处将多个线程执行结果汇总,CountDownLatch 在各线程执行完毕后向总线程汇报结果。

九、如何合理的配置 java 线程池?如 CPU 密集型的任务,基本线程池应该配置多大?IO 密集型的任务,基本线程池应该配置多大?用有界队列好还是无界队列好?任务非常多的时候,使用什么阻塞队列能获取最好的吞吐量?

1.配置线程池时 CPU 密集型任务可以少配置线程数,大概和机器的 cpu 核数相当,可以使得每个线程都在执行任务

2.IO 密集型时,大部分线程都阻塞,故需要多配置线程数,2*cpu 核数

3.有界队列和无界队列的配置需区分业务场景,一般情况下配置有界队列,在一些可能会有爆发性增长的情况下使用无界队列。

4.任务非常多时,使用非阻塞队列使用 CAS 操作替代锁可以获得好的吞吐量。

十、同步有几种实现方法?

同步的实现方法有五种:1.同步方法;2.同步代码块;3.使用特殊域变量(volatile)实现线程同步;4.使用重入锁实现线程同步;5.使用局部变量实现线程同步 。

同步的实现方面有两种,分别是 synchronized,wait 与 notify wait():使一个线程处于等待状态,并且释放所持有的对象的 lock。

 

十一、volatile 有什么用?能否用一句话说明下 volatile 的应用场景?

Volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性。可以被看作是一种 “程度较轻的 synchronized”;与 synchronized 块相比,volatile 变量所需的编码较少,并且运行时开销也较少,但是它所能实现的功能也仅是 synchronized 的一部分。

您只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:

A. 对变量的写操作不依赖于当前值。

B. 该变量没有包含在具有其他变量的不变式中。

首先总结一下 Volatile 的特性:可见性,但不互斥.怎么理解这句话,首先可见性的原因是以为,这个关键字可以让变量不缓存在寄存器里面,每次取值都是直接从主存里面获取,所以每次都是最新的值.但是不互斥是因为没有锁,这里有个改变值的流程(读取-修改-写入),这是一个比读更耗时的一个操作,在没有加锁的情况下别的线程读取这个值可能是任何一个时刻的值;所以根据这个特性可以推导出使用 Volatile 在少写多读的情况下,性能非常好,当然首先要保证不会是多线程同时写.

Volatile 有五个使用场景

1.作为状态标志

2.一次性安全发布

3.独立观察

4.volatile bean 模式

5. 开销较低的读写锁策略

十二、什么场景下可以使用 volatile 替换 synchronized?

只需要保证共享资源的可见性的时候可以使用 volatile 替代,synchronized 保证可操作的原子性一致性和可见性。 volatile 适用于新值不依赖于就值的情形

十三、线程池的启动策略?

1、线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。

2、当调用 execute() 方法添加一个任务时,线程池会做如下判断:

a. 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;

b. 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列。

c. 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建线程运行这

个任务;

d. 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常,告

诉调用者“我不能再接受任务了”。

3、当一个线程完成任务时,它会从队列中取下一个任务来执行。

4、当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于

corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

十四、Java 线程池中 submit() 和 execute()方法有什么区别?

两个方法都可以向线程池提交任务,execute()方法的返回类型是 void,它定义在 Executor 接口中, 而 submit()方法可以返回持有计算结果的 Future 对象,它定义在 ExecutorService 接口中,它扩展了 Executor 接口,其它线程池类像 ThreadPoolExecutor 和 ScheduledThreadPoolExecutor 都有这些方法。

十五、thread 类中的 yield 方法有什么作用?

Yield 方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃 CPU 占用而不能保证使其它线程一定能占用 CPU,执行 yield()的线程有可能在进入到暂停状态后马上又被执行。


一、乐观锁和悲观锁的理解及如何实现,有哪些实现方式?


1.乐观锁,每次操作时不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止


2.悲观锁是会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。


3.乐观锁可以使用 volatile+CAS 原语实现,带参数版本来避免 ABA 问题,在读取和替换的时候进行判定版本是否一致


4.悲观锁可以使用 synchronize 的以及 Lock


二、什么是可重入锁(ReentrantLock)?


     重入锁指的是在某一个线程中可以多次获得同一把锁,在线程中多次操作有锁的方法
复制代码


三、reentrantlock 的优点和缺点


reentrantlock 的优点


1.可以添加多个检控条件, 如果使用 synchronized,则只能使用一个. 使用 reentrant locks 可以有多个 wait()/notify() 队列. [译注:直接多 new 几个 ReentrantLock 就可以了,不同的场景/条件用不同的 ReentrantLock ]


2.可以控制线程得到锁的顺序,也就是有公平锁(按照进入顺序得到资源),也可以不按照顺就像.synchronized 一样.


3.可以查看锁的状态, 锁是否被锁上了.


4.可以查看当前有多少线程再等待锁.


reentrantlock 的缺点


1.需要使用 import 引入相关的 Class


2.不能忘记在 finally 模块释放锁,这个看起来比 synchronized 丑陋


3.synchronized 可以放在方法的定义里面, 而 reentrantlock 只能放在块里面. 比较起来, synchronized 可以减少嵌套


四、我们面对 ReentrantLock 和 synchronized 改如何选择?1.Synchronized 相比 Lock,为许多开发人员所熟悉,并且简洁紧凑,如果现有程序已经使用了内置锁,那么尽量保持代码风格统一,尽量不引入 Lock,避免两种机制混用,容易令人困惑,也容易发生错误。


2.在 Synchronized 无法满足需求的情况下,Lock 可以作为一种高级工具,这些功能包括“可定时的、可轮询的与可中断的锁获取操作,公平队列,以及非块结构的锁”否则还是优先使用 Synchronized。


3.最后,未来更可能提升 Synchronized 而不是 Lock 的性能,因为 Synchronized 是 JVM 的内置属性,他能执行一些优化,例如对线程封闭的锁对象的锁消除优化,通过增加锁的粒度来消除内置锁的同步,而如果基于类库的锁来实现这些功能,则可能性不大。


五、synchronized 和 java.util.concurrent.locks.Lock 的异同?


1.Lock 和 synchronized 有一点明显的区别 —— lock 必须在 finally 块中释放。否则,如果受保护的代码将抛出异常,锁就有可能永远得不到释放!这一点区别看起来可能没什么,但是实际上,它极为重要。忘记在 finally 块中释放锁,可能会在程序中留下一个定时炸弹,当有一天炸弹爆炸时,您要花费很大力气才有找到源头在哪。而使用同步,JVM 将确保锁会获得自动释放。


2.一个 Lock 对象和一个 synchronized 代码块之间的主要不同点是:


synchronized 代码块不能够保证进入访问等待的线程的先后顺序。


你不能够传递任何参数给一个 synchronized 代码块的入口。因此,对于 synchronized 代码块的访问等待设置超时时间是不可能的事情。


synchronized 块必须被完整地包含在单个方法里。而一个 Lock 对象可以把它的 lock() 和 unlock() 方法的调用放在不同的方法里。


六、SynchronizedMap 和 ConcurrentHashMap(效率高)有什么区别?


1.java5 中新增了 ConcurrentMap 接口和它的一个实现类 ConcurrentHashMap。ConcurrentHashMap 提供了和 Hashtable 以及 SynchronizedMap 中所不同的锁机制。比起 synchronizedMap 来,它提供了好得多的并发性。多个读操作几乎总可以并发地执行,同时进行的读和写操作通常也能并发地执行,而同时进行的写操作仍然可以不时地并发进行(相关的类也提供了类似的多个读线程的并发性,但是,只允许有一个活动的写线程)。Hashtable 中采用的锁机制是一次锁住整个 hash 表,从而同一时刻只能由一个线程对其进行操作;而 ConcurrentHashMap 中则是一次锁住一个桶。ConcurrentHashMap 默认将 hash 表分为 16 个桶,诸如 get,put,remove 等常用操作只锁当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有 16 个写线程执行,并发性能的提升是显而易见的。前面说到的 16 个线程指的是写线程,而读操作大部分时候都不需要用到锁。只有在 size 等操作时才需要锁住整个 hash 表。


  2.在迭代方面,ConcurrentHashMap使用了一种不同的迭代方式。在这种迭代方式中,当iterator被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数据 ,iterator完成后再将头指针替换为新的数据 ,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变。
复制代码


ConcurrentHashMap 把实际 map 划分成若干部分来实现它的可扩展性和线程安全。这种划分是使用并发度获得的,它是 ConcurrentHashMap 类构造函数的一个可选参数,默认值为 16,这样在多线程情况下就能避免争用。另外一个不同点是,在被遍历的时候,即使是 ConcurrentHashMap 被改动,它也不会抛 ConcurrentModificationException。尽管 Iterator 的设计不是为多个线程的同时使用。


七、CopyOnWriteArrayList 可以用于什么应用场景?


多读少写的场景。读写并不是在同一个对象上。在写时会大面积复制数组,所以写的性能差,在写完成后将读的引用改为执行写的对象。CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出 ConcurrentModificationException。在 CopyOnWriteArrayList 中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。


八、.如何让一段程序并发的执行,并最终汇总结果?


使用 CyclicBarrier 和 CountDownLatch 都可以,使用 CyclicBarrier 在多个关口处将多个线程执行结果汇总,CountDownLatch 在各线程执行完毕后向总线程汇报结果。


九、如何合理的配置 java 线程池?如 CPU 密集型的任务,基本线程池应该配置多大?IO 密集型的任务,基本线程池应该配置多大?用有界队列好还是无界队列好?任务非常多的时候,使用什么阻塞队列能获取最好的吞吐量?


1.配置线程池时 CPU 密集型任务可以少配置线程数,大概和机器的 cpu 核数相当,可以使得每个线程都在执行任务


    2.IO密集型时,大部分线程都阻塞,故需要多配置线程数,2*cpu核数
3.有界队列和无界队列的配置需区分业务场景,一般情况下配置有界队列,在一些可能会有爆发性增长的情况下使用无界队列。
4.任务非常多时,使用非阻塞队列使用CAS操作替代锁可以获得好的吞吐量。
复制代码


十、同步有几种实现方法?


同步的实现方法有五种:1.同步方法;2.同步代码块;3.使用特殊域变量(volatile)实现线程同步;4.使用重入锁实现线程同步;5.使用局部变量实现线程同步 。


同步的实现方面有两种,分别是synchronized,wait与notify wait():使一个线程处于等待状态,并且释放所持有的对象的lock。
复制代码


十一、volatile 有什么用?能否用一句话说明下 volatile 的应用场景?


Volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性。可以被看作是一种 “程度较轻的 synchronized”;与 synchronized 块相比,volatile 变量所需的编码较少,并且运行时开销也较少,但是它所能实现的功能也仅是 synchronized 的一部分。


      您只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:
A. 对变量的写操作不依赖于当前值。
B. 该变量没有包含在具有其他变量的不变式中。
复制代码


首先总结一下 Volatile 的特性:可见性,但不互斥.怎么理解这句话,首先可见性的原因是以为,这个关键字可以让变量不缓存在寄存器里面,每次取值都是直接从主存里面获取,所以每次都是最新的值.但是不互斥是因为没有锁,这里有个改变值的流程(读取-修改-写入),这是一个比读更耗时的一个操作,在没有加锁的情况下别的线程读取这个值可能是任何一个时刻的值;所以根据这个特性可以推导出使用 Volatile 在少写多读的情况下,性能非常好,当然首先要保证不会是多线程同时写.


Volatile 有五个使用场景


1.作为状态标志 2.一次性安全发布 3.独立观察 4.volatile bean 模式 5. 开销较低的读写锁策略


十二、什么场景下可以使用 volatile 替换 synchronized?


只需要保证共享资源的可见性的时候可以使用 volatile 替代,synchronized 保证可操作的原子性一致性和可见性。 volatile 适用于新值不依赖于就值的情形


十三、线程池的启动策略?


1、线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。


2、当调用 execute() 方法添加一个任务时,线程池会做如下判断:


a. 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;


b. 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列。


c. 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建线程运行这


个任务;


d. 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常,告


诉调用者“我不能再接受任务了”。


3、当一个线程完成任务时,它会从队列中取下一个任务来执行。


4、当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于


corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。


十四、Java 线程池中 submit() 和 execute()方法有什么区别?


两个方法都可以向线程池提交任务,execute()方法的返回类型是 void,它定义在 Executor 接口中, 而 submit()方法可以返回持有计算结果的 Future 对象,它定义在 ExecutorService 接口中,它扩展了 Executor 接口,其它线程池类像 ThreadPoolExecutor 和 ScheduledThreadPoolExecutor 都有这些方法。


十五、thread 类中的 yield 方法有什么作用?


Yield 方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃 CPU 占用而不能保证使其它线程一定能占用 CPU,执行 yield()的线程有可能在进入到暂停状态后马上又被执行。


十六、如何避免死锁


在有些情况下死锁是可以避免的。两种用于避免死锁的技术:1)加锁顺序(线程按照一定的顺序加锁)2)加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)


十七、Synchronized 有哪些缺点?


1.只有一个 condition 与锁相关联,这个 condition 是什么?就是 synchronized 对针对的对象锁。2.synchronized 无法中断一个正在等待获得锁的线程,也即多线程竞争一个锁时,其余未得到锁的线程只能不停的尝试获得锁,而不能中断。这种情况对于大量的竞争线程会造成性能的下降等后果。


十八、什么是 Deamo 线程,它有什么意义。有两类线程:User Thread(用户线程)、Daemon Thread(守护线程) 。Daemon 的作用是为其他线程的运行提供便利服务,比如垃圾回收线程就是一个很称职的守护者。User 和 Daemon 两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果 User Thread 已经全部退出运行了,只剩下 Daemon Thread 存在了,虚拟机也就退出了。


十九、描述线程池分类,


分类:


1.创建无大小限制的线程池(newCachedThreadPool())2.创建固定大小的线程池(newFixedThreadPool(int nThreads))3.单线程池 (newSingleThreadScheduledExecutor())4.创建定时调度池 (newScheduledThreadPool(int corePoolSize)


二十、线程池有几种拒绝策略,分别是哪些?


四种:


1.ThreadPoolExecutor.AbortPolic(默认):当任务添加到线程池之中被拒绝的时候,这个时候会抛出 RejectedExecutionException 异常 2.ThereeadPoolExecutor.CallerRunsPolicy:当任务被拒绝的时候,会在线程池当前正在执行线程的 WORKER 里面处此线程 3.ThreadPoolExecutor.DiscardOldestPoliy:当被拒绝的时候,线程池会放弃队列之中等待最长时间的任务,并且将被拒绝的任务添加到队列之中 4.ThreadPoolExecutor.DiscardPolicy:当任务添加拒绝的时候,将直接丢弃此线程


用户头像

andy

关注

还未添加个人签名 2019-11-21 加入

还未添加个人简介

评论

发布
暂无评论
JUC面试题_andy_InfoQ写作社区