并发编程面试题
1. Synchronized 用过吗,其原理是什么?
反编译后:synchronized代码块主要是靠 monitorenter 和 monitorexit 这两个 字节码指令 来实现同步的 。
锁对象:
这两个指令是什么意思呢?在虚拟机执行到 monitorenter 指令时,首先要尝试获取对象的锁,如果这个对象没有锁定,或者当前线程已经拥有了这个对象的锁,把锁的计数器 +1。当执行 monitorexit 指令时将锁计数器 -1。 当计数器为 0 时,锁就被释放了。
如果获取对象失败了,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。
HotSpot对象头:
对象头大小:非数组类型8字节,数组类型12字节。
2. 你刚才提到获取对象的锁,这个“锁”到底是什么?如何确定对象的锁?
锁是存在对象头里 Mark Word中的一块区域。
- 锁普通方法(锁是当前实例对象)
- 锁静态方法(锁是当前类的Class对象)
- 锁代码块(锁是括号里配置的对象)
3. 什么是可重入性,为什么说 Synchronized 是可重入锁?
在执行 monitorenter 指令时,如果这个对象没有锁定, 或者当前线程已经拥有了这个对象的锁(而不是已拥有了锁则不能继续获取),就把锁的计数器 +1, 其实本质上就通过这种方式实现了可重入性。
4. JDK1.6 以后对 Synchronized 做了哪些优化?锁可不可以降级?
JDK1.6 之后加入了偏向锁和轻量级锁。无锁、偏向锁、轻量级锁、重量级锁。随着竞争越来越激烈,锁会逐渐的升级,锁升级之后就无法降级了。
偏向锁:Synchronized 的锁是存在对象头里 Mark Word中的一块区域。因为大多数情况下,锁不存在多线程竞争,而是由同一线程多次获得。而获取锁是有一定的开销的,那么为了减少获取锁的开销就引入了偏向锁。当一个线程访问同步块获取锁的时候,会在对象头中和栈帧记录里存储 这个偏向的线程ID。下次该线程再次进入和退出的时候就不需要进行CAS机制来加锁和解锁,这就降低了获取锁和释放锁的性能消耗。
轻量级锁:在没有竞争的情况下使用CAS操作避免了使用互斥量的开销,如果存在锁竞争,那么除了有互斥量的开销外,还额外发生了CAS操作。这种情况下会比传统的重量级锁更慢。
两者区别:无竞争的情况下,轻量级锁使用CAS操作去消除互斥量的开销,而偏向锁连CAS操作都不做了。
5. 为什么说 Synchronized 是非公平锁?
非公平主要表现在获取锁的行为上,并非是按照申请锁的时间前后给等待线程分配锁的,每当锁被释放后,任何一个线程都有机会竞争到锁,这样 做的目的是为了提高执行性能,缺点是可能会产生线程饥饿现象。
6. 什么是锁消除和锁粗化?
锁消除:指虚拟机即时编译器在运行时,会对检测到不可能存在数据竞争的锁进行消除。
锁粗化:锁粗化就是增大锁的作用域。
7. 为什么说 Synchronized 是一个悲观锁?乐观锁的实现原理又是什么?什么是 CAS,它有什么特性?
Synchronized 显然是一个悲观锁,因为它的并发策略是悲观的。
乐观锁的核心算法是 CAS(Compareand Swap,比较并交换)。
CAS涉及到旧值、新值。
CAS具有原子性,是由CPU指令实现保证。 JDK中提供了 Unsafe 类执行这些操作。
8. 乐观锁一定就是好的吗?
1)ABA问题
2)循环时间开销大
3)只能保证一个共享变量的原子操作
9. synchronized 和 ReentrantLock 区别是什么?
-
ReentrantLock在性能上要好于synchronized。
-
ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁。
-
ReentrantLock 只适用于代码块锁,而 synchronized 可用于修饰方法、代码块等。
10. ReentrantLock 是如何实现可重入性的?
跟 Synchronized 相比,可重入锁 ReentrantLock 其实现原理是基于所谓的 AQS 框架。ReentrantLock 内部自定义了一个同步器 Sync,每次获取锁的时候,看下当前维护的那个线程ID和当前请求的线程ID是否一样 ,一样就可重入了。
11. 那么请谈谈 AQS 框架是怎么回事儿?
队列同步器 AQS (AbstractQueuedSynchronizer) 是一个用来构建锁或其他同步组件的框架。
主要实现:一个 volatile int 变量,一个FIFO双向队列。
-
AQS 在内部定义了一个 volatile int state 变量 ,表示同步状态:当线程调用 lock 方法时,如果 state= 0 ,说明没有任何线程占有共享资源的锁,可以获得锁并将 state= 1 ; 如果 state= 1, 则说明有线程目前正在使用共享变量,其他线程必须加入同步队列进行等待。
-
AQS 通过 Node 内部类构成的一个双向的同步队列,当有线程获取锁失败后,就被添加到队列末尾。
12. 除了 ReetrantLock,你还接触过 JUC 中的哪些并发工具?
- ConcurrentHashMap
- ReentrantLock
- ConcurrentLinkedQueue
- Executor 强大的Executor框架,可以创建各种不同类型的线程池,调度任务运行等。
请谈谈 ReadWriteLock 和 StampedLock。
如何让 Java 的线程彼此同步?你了解过哪些同步器?请分别介绍下。
CountDownLatch,CyclicBarrier。
CyclicBarrier 和 CountDownLatch 看起来很相似,请对比下呢?
CyclicBarrier 和 CountDownLatch 都可以用来让一组线程等待其它线程完成。与 CyclicBarrier 不同的是,CountdownLatch 计数器不能重新使用,CyclicBarrier 的计数器可以使用reset()
重置。
什么是 Java 的内存模型,Java 中各个线程是怎么彼此看到对方的变量的?
13. 请谈谈 volatile 有什么特点?
被volatile 修饰的变量具有以下特性:
-
可见性。
任意线程都能够看到volatile 变量的写入。原理就是现代CPU的MESI协议与总线嗅探机制。被 volatile 修饰的变量,在写的时候会把变量值刷新到主内存,在读的时候会从主内存中取值。绕过了本地内存。
-
禁止指令重排序优化 。
volatile 限制了指令的重排序。原理就是在字节码层面:赋值后多执行了一个 “lock” 前缀指令操作,这个操作相当于一个内存屏障(指重排序时不能把后面的指令重排序到内存屏障之前的位置)。
14. MESI协议:Cache 一致性协议
总线嗅探机制:
把所有的读写请求都通过总线(Bus
)广播给所有的 CPU 核心,然后让各个核心去“嗅探”这些请求,再根据本地的情况进行响应。
MESI 协议:
- 基于写失效(
Write Invalidate
) 如果某个 CPU 核心 写入一个 Cache Line,则会广播一个失效
请求告诉所有其他的 CPU 核心。 - Cache Line 的 标记
- M:代表已修改(
Modified
) 缓存行是脏的(dirty
),与主存的值不同。如果别的CPU内核要读主存这块数据,该缓存行必须回写到主存,状态变为共享(S
). - E:代表独占(
Exclusive
) 缓存行只在当前缓存中,但是干净的(clean
)–缓存数据同于主存数据。当别的缓存读取它时,状态变为共享(S
);当前写数据时,变为已修改(M
)状态。 - S:代表共享(
Shared
) 缓存行也存在于其它缓存中且是干净(clean
)的。缓存行可以在任意时刻抛弃。 - I:代表已失效(
Invalidated
) 缓存行是脏的(dirty),无效的。
- M:代表已修改(
任意一对缓存,对应缓存行的相容关系:
比如: 当块标记为 M
(已修改), 在其他缓存中的数据副本被标记为 I
(无效).
15. 基于 volatile 变量的运算都是并发安全的?
针对单个 volatile 变量的读写是安全的,但是对于 volatile ++ 这种复合操作不是安全的。
int t = 1; //单个变量读写
t = t + 1; //复合操作
处理器在处理代码的时候,需要处理以下三个操作:
从内存中读取t的值。
进行 t = t + 1这个运算
把 t 的值写回到内存中
16. 请对比下 volatile 对比 Synchronized 的异同?
-
Synchronized
可以修饰代码块,普通方法,静态方法。保证原子性。
-
volatile
只能修饰变量。无法保证复合运算的原子性。
17. 请谈谈 ThreadLocal 是怎么解决并发安全的?
ThreadLocal 为单个线程提供了存储的能力,也就是将一个线程与一个变量绑定到一起。起到线程隔离的作用。
18. 很多人都说要慎用 ThreadLocal,谈谈你的理解,使用 ThreadLocal 需要注意些什么?
ThreadLocal的实现是基于一个所谓的ThreadLocalMap。
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
内存泄漏问题:使用线程池的时候,核心线程一直存活。
1.为了防止内存泄漏,key被设计成弱引用会被直接回收
2.但是value还在,故使用 ThreadLocal 要注意 remove。
19. ThreadLocal和Synchronized区别?
ThreadLocal和Synchronized都是为了解决多线程中相同变量的访问冲突问题,不同的点是:
- Synchronized是通过线程等待,牺牲时间来解决访问冲突。
- ThreadLocal是通过每个线程单独开辟一份存储空间,牺牲空间来解决冲突,并且相比于Synchronized,ThreadLocal具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值。