#yyds干货盘点# Java并发面试题第二弹
Posted 程序员大彬
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了#yyds干货盘点# Java并发面试题第二弹相关的知识,希望对你有一定的参考价值。
大家好,我是大彬~
今天给大家分享Java并发高频面试题(第二弹),助力春招上岸!
文章目录如下:
- volatile底层原理
- synchronized的用法有哪些?
- synchronized的作用有哪些?
- synchronized 底层实现原理?
- volatile和synchronized的区别?
- ReentrantLock和synchronized区别?
- wait()和sleep()的异同点?
- Runnable和Callable有什么区别?
- 守护线程是什么?
- 线程间通信方式
- ThreadLocal
- ThreadLocal原理
- ThreadLocal内存泄漏的原因?
- ThreadLocal使用场景有哪些?
- AQS原理
- ReentrantLock 是如何实现可重入性的?
- 锁的分类
- 公平锁与非公平锁
- 共享式与独占式锁
- 悲观锁与乐观锁
- 乐观锁有什么问题?
- 什么是CAS?
volatile底层原理
volatile
是轻量级的同步机制,volatile
保证变量对所有线程的可见性,不保证原子性。
- 当对
volatile
变量进行写操作的时候,JVM会向处理器发送一条LOCK
前缀的指令,将该变量所在缓存行的数据写回系统内存。 - 由于缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己的缓存是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行置为无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存中。
来看看缓存一致性协议是什么。
缓存一致性协议:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,就会从内存重新读取。
volatile
关键字的两个作用:
- 保证了不同线程对共享变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
- 禁止进行指令重排序。
指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。Java编译器会在生成指令系列时在适当的位置会插入
内存屏障
指令来禁止处理器重排序。插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。对一个volatile字段进行写操作,Java内存模型将在写操作后插入一个写屏障指令,这个指令会把之前的写入值都刷新到内存。
synchronized的用法有哪些?
- 修饰普通方法:作用于当前对象实例,进入同步代码前要获得当前对象实例的锁
- 修饰静态方法:作用于当前类,进入同步代码前要获得当前类对象的锁,synchronized关键字加到static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁
- 修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁
synchronized的作用有哪些?
原子性:确保线程互斥的访问同步代码;
可见性:保证共享变量的修改能够及时可见;
有序性:有效解决重排序问题。
synchronized 底层实现原理?
synchronized 同步代码块的实现是通过 monitorenter
和 monitorexit
指令,其中 monitorenter
指令指向同步代码块的开始位置,monitorexit
指令则指明同步代码块的结束位置。当执行 monitorenter
指令时,线程试图获取锁也就是获取 monitor
的持有权(monitor对象存在于每个Java对象的对象头中, synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因)。
其内部包含一个计数器,当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit
指令后,将锁计数器设为0
,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止
synchronized 修饰的方法并没有 monitorenter
指令和 monitorexit
指令,取得代之的确实是ACC_SYNCHRONIZED
标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED
访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
volatile和synchronized的区别?
-
volatile
只能使用在变量上;而synchronized
可以在类,变量,方法和代码块上。 -
volatile
至保证可见性;synchronized
保证原子性与可见性。 -
volatile
禁用指令重排序;synchronized
不会。 -
volatile
不会造成阻塞;synchronized
会。
ReentrantLock和synchronized区别?
- 使用synchronized关键字实现同步,线程执行完同步代码块会自动释放锁,而ReentrantLock需要手动释放锁。
- synchronized是非公平锁,ReentrantLock可以设置为公平锁。
- ReentrantLock上等待获取锁的线程是可中断的,线程可以放弃等待锁。而synchonized会无限期等待下去。
- ReentrantLock 可以设置超时获取锁。在指定的截止时间之前获取锁,如果截止时间到了还没有获取到锁,则返回。
- ReentrantLock 的 tryLock() 方法可以尝试非阻塞的获取锁,调用该方法后立刻返回,如果能够获取则返回true,否则返回false。
wait()和sleep()的异同点?
相同点:
- 使当前线程暂停运行,把机会交给其他线程
- 任何线程在等待期间被中断都会抛出
InterruptedException
不同点:
-
wait()
是Object超类中的方法;而sleep()
是线程Thread类中的方法 - 对锁的持有不同,
wait()
会释放锁,而sleep()
并不释放锁 - 唤醒方法不完全相同,
wait()
依靠notify
或者notifyAll
、中断、达到指定时间来唤醒;而sleep()
到达指定时间被唤醒 - 调用
wait()
需要先获取对象的锁,而Thread.sleep()
不用
Runnable和Callable有什么区别?
- Callable接口方法是
call()
,Runnable的方法是run()
; - Callable接口call方法有返回值,支持泛型,Runnable接口run方法无返回值。
- Callable接口
call()
方法允许抛出异常;而Runnable接口run()
方法不能继续上抛异常。
守护线程是什么?
守护线程是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。在 Java 中垃圾回收线程就是特殊的守护线程。
线程间通信方式
volatile
volatile 使用共享内存实现线程间相互通信。多个线程同时监听一个变量,当这个变量被某一个线程修改的时候,其他线程可以感知到这个变化。
wait和 notify
wait/notify
为Object对象的方法,调用wait/notify
需要先获得对象的锁。对象调用wait()
之后线程释放锁,将线程放到对象的等待队列,当通知线程调用此对象的notify()
方法后,等待线程并不会立即从wait()
返回,需要等待通知线程释放锁(通知线程执行完同步代码块),等待队列里的线程获取锁,获取锁成功才能从wait()
方法返回,即从wait()
方法返回前提是线程获得锁。
join
当在一个线程调用另一个线程的join()
方法时,当前线程阻塞等待被调用join方法的线程执行完毕才能继续执行。join()
是基于等待通知机制实现的。
ThreadLocal
线程本地变量。当使用ThreadLocal
维护变量时,ThreadLocal
为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程。
ThreadLocal原理
每个线程都有一个ThreadLocalMap
(ThreadLocal
内部类),Map中元素的键为ThreadLocal
,而值对应线程的变量副本。
调用threadLocal.set()
-->调用getMap(Thread)
-->返回当前线程的ThreadLocalMap<ThreadLocal, value>
-->map.set(this, value)
,this是threadLocal
本身。源码如下:
public void set(T value)
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
ThreadLocalMap getMap(Thread t)
return t.threadLocals;
void createMap(Thread t, T firstValue)
t.threadLocals = new ThreadLocalMap(this, firstValue);
调用get()
-->调用getMap(Thread)
-->返回当前线程的ThreadLocalMap<ThreadLocal, value>
-->map.getEntry(this)
,返回value
。源码如下:
public T get()
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
("unchecked")
T result = (T)e.value;
return result;
return setInitialValue();
threadLocals
的类型ThreadLocalMap
的键为ThreadLocal
对象,因为每个线程中可有多个threadLocal
变量,如longLocal
和stringLocal
。
public class ThreadLocalDemo
ThreadLocal<Long> longLocal = new ThreadLocal<>();
public void set()
longLocal.set(Thread.currentThread().getId());
public Long get()
return longLocal.get();
public static void main(String[] args) throws InterruptedException
ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo();
threadLocalDemo.set();
System.out.println(threadLocalDemo.get());
Thread thread = new Thread(() ->
threadLocalDemo.set();
System.out.println(threadLocalDemo.get());
);
thread.start();
thread.join();
System.out.println(threadLocalDemo.get());
ThreadLocal
并不是用来解决共享资源的多线程访问问题,因为每个线程中的资源只是副本,不会共享。因此ThreadLocal
适合作为线程上下文变量,简化线程内传参。
ThreadLocal内存泄漏的原因?
每个线程都有⼀个ThreadLocalMap
的内部属性,map的key是ThreaLocal
,定义为弱引用,value是强引用类型。垃圾回收的时候会⾃动回收key,而value的回收取决于Thread对象的生命周期。一般会通过线程池的方式复用线程节省资源,这也就导致了线程对象的生命周期比较长,这样便一直存在一条强引用链的关系:Thread
--> ThreadLocalMap
-->Entry
-->Value
,随着任务的执行,value就有可能越来越多且无法释放,最终导致内存泄漏。
解决⽅法:每次使⽤完ThreadLocal
就调⽤它的remove()
⽅法,手动将对应的键值对删除,从⽽避免内存泄漏。
ThreadLocal使用场景有哪些?
ThreadLocal
适用场景:每个线程需要有自己单独的实例,且需要在多个方法中共享实例,即同时满足实例在线程间的隔离与方法间的共享,这种情况适合使用ThreadLocal
。比如Java web应用中,每个线程有自己单独的Session
实例,就可以使用ThreadLocal
来实现。
AQS原理
AQS,AbstractQueuedSynchronizer
,抽象队列同步器,定义了一套多线程访问共享资源的同步器框架,许多并发工具的实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch
。
AQS使用一个volatile
的int类型的成员变量state
来表示同步状态,通过CAS修改同步状态的值。当线程调用 lock 方法时 ,如果 state=0
,说明没有任何线程占有共享资源的锁,可以获得锁并将 state
加1。如果 state
不为0,则说明有线程目前正在使用共享变量,其他线程必须加入同步队列进行等待。
private volatile int state;//共享变量,使用volatile修饰保证线程可见性
同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态(独占或共享)构造成为一个节点(Node)并将其加入同步队列并进行自旋,当同步状态释放时,会把首节点中的后继节点对应的线程唤醒,使其再次尝试获取同步状态。
ReentrantLock 是如何实现可重入性的?
ReentrantLock
内部自定义了同步器sync,在加锁的时候通过CAS算法,将线程对象放到一个双向链表中,每次获取锁的时候,会检查当前占有锁的线程和当前请求锁的线程是否一致,如果一致,同步状态加1,表示锁被当前线程获取了多次。
源码如下:
final boolean nonfairTryAcquire(int acquires)
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0)
if (compareAndSetState(0, acquires))
setExclusiveOwnerThread(current);
return true;
else if (current == getExclusiveOwnerThread())
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
return false;
锁的分类
公平锁与非公平锁
#yyds干货盘点#Linux常见面试题之文件管理命令