Java并发面试题汇总
一. 多线程基础
1.创建线程的三种方式的对比?
- 采用实现Runnable或Callable接口的方式创建多线程
- 优点:可以继承其他类,并且多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU. 代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
- 缺点:编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法
- 区别:
- Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()
- Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
- Call方法可以抛出异常,run方法不可以。
- 运行Callable任务可以拿到一个Future对象,表示异步计算的结果
- 使用继承Thread类的方式创建多线程
- 优点:编写简单,如果要访问当前线程,直接使用this即获得当前线程。
- 缺点:已经继承了Thread类,所以不能再继承其他父类
2.线程的状态流转
-
新建状态(New):当线程对象对创建后,即进入了新建状态
-
就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态
-
运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态
-
阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态
- 等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态
- 同步阻塞 — 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
- 其他阻塞 — 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态
-
死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期
3.为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
- 调用 start() 方法,会启动线程并使线程进入了就绪状态,然后自动执行run() 方法的内容
- 直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
4. 什么是线程死锁?如何避免死锁?
- 死锁:多个线程都占用了对方的锁资源
- 避免死锁:通过指定锁的获取顺序,比如规定,只有获得A锁的线程才有资格获取B锁,按顺序获取锁就可以避免死锁
5.sleep() 方法和 wait() 方法区别和共同点?
-
sleep方法:是Thread类的静态方法;wait方法:是Object的方法,必须与synchronized关键字一起使用
-
sleep 方法没有释放锁;而 wait 方法释放了锁
-
sleep() 方法执行完成后,线程会自动苏醒;wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者notifyAll() 方法
-
两者都可以暂停线程的执行
6.Thread类中的yield方法有什么作用?
- Yield方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行,但并不保证成功
7.谈谈volatile的使用及其原理?
- 保证可见性,不保证原子性
- 禁止指令重排
8.如何创建线程实例并运行?
- 创建 Thread 的子类并重写 run();创建类对象并调用start方法
- 实现 Runnable 接口;创建Thread对象,通过构造器放入资源对象,调用start方法
9.线程阻塞的三种情况?
- 等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
- 同步阻塞 — 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
- 其他阻塞 — 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。
10.守护线程是什么?
- 一般是为工作线程服务的,当所有的用户线程结束,守护线程自动结束。常见的守护线程:垃圾回收机制
11.CAS了解吗?
- CAS:全称 Compare and swap ,即比较并交换,它是一条 CPU 同步原语。是一种硬件对并发的支持,针对多处理器操作而设计的一种特殊指令,用于管理对共享数据的并发访问。
12.synchronized 和 volatile 的区别是什么?
- volatile 解决的是内存可见性问题,会使得所有对 volatile 变量的读写都直接写入主存,即 保证了变量的可见性。
- synchronized 解决的事执行控制的问题,它会阻止其他线程获取当前对象的监控锁,这样一来就让当前对象中被 synchronized 关键字保护的代码块无法被其他线程访问,也就是无法并发执行
- 区别:
- volatile 本质是在告诉 JVM 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
- volatile 仅能使用在变量级别;synchronized 则可以使用在 变量. 方法. 和类级别的
- volatile 仅能实现变量的修改可见性,不能保证原子性;而synchronized 则可以 保证变量的修改可见性和原子性
- volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
- volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。
13.synchronized 和 Lock 有什么区别?
- synchronized 可以给类. 方法. 代码块加锁;而 lock 只能给代码块加锁
- synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
- 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
15.synchronized的用法有哪些?
- 修饰普通方法:作用于当前对象实例,进入同步代码前要获得当前对象实例的锁
- 修饰静态方法:作用于当前类,进入同步代码前要获得当前类对象的锁,synchronized 关键字加到static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁
- 修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁
16.Synchronized的作用有哪些?
- 原子性:确保线程互斥的访问同步代码
- 可见性:保证共享变量的修改能够及时可见
- 有序性:有效解决重排序问题,即一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作。
17.多线程中 synchronized 锁升级的原理是什么?
- synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。
- 锁的升级的目的:减低了锁带来的性能消耗
18.synchronized 为什么是非公平锁?非公平体现在哪些地方?
- 当持有锁的线程释放锁时,该线程会执行以下两个重要操作: 先将锁的持有者 owner 属性赋值为 null; 唤醒等待链表中的一个线程(假定继承者);在这之间如果有其他线程刚好在尝试获取锁(例如自旋),则可以马上获取到锁
- 当线程尝试获取锁失败,进入阻塞时,放入链表的顺序,和最终被唤醒的顺序是不一致的,也就是说你先进入链表,不代表你就会先被唤醒。
19. JVM对synchronized的优化有哪些?
- 锁膨胀:膨胀方向是由无锁——>偏向锁——>轻量级锁——>重量级锁,并且膨胀方向不可逆。
- 偏向锁:减少统一线程获取锁的代价。在大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得,那么此时就是偏向锁
- 轻量级锁:当存在第二个线程申请同一个锁对象时,偏向锁就会立即升级为轻量级锁
- 重量级锁:当同一时间有多个线程竞争锁时,锁就会被升级成重量级锁,此时其申请锁带来的开销也就变大。重量级锁一般使用场景会在追求吞吐量,同步块或者同步方法执行时间较长的场景
- 锁消除:消除锁是虚拟机另外一种锁的优化,这种优化更彻底,在编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。
- 锁粗化:锁粗化是虚拟机对另一种极端情况的优化处理,通过扩大锁的范围,避免反复加锁和释放锁
- 自旋锁与自适应自旋锁:轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。
- 自旋锁:许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得,通过让线程执行循环等待锁的释放,不让出CPU。
- 自适应自旋锁:它的自旋的次数不再固定,其自旋的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,这就解决了自旋锁带来的缺点。
20.ThreadLocal是什么?
- ThreadLocal,即线程本地变量。如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了线程安全问题。
- static ThreadLocal
localVariable = new ThreadLocal<>();
21.ThreadLocal的实现原理?
- Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,即每个线程都有一个属于自己的ThreadLocalMap。
- ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值。
- 每个线程在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
22. 知道ThreadLocal 内存泄露问题吗?
-
原因:ThreadLocalMap中使用的 key 为 ThreadLocal 的弱引用
-
解决方法:使用完ThreadLocal后,及时调用remove()方法释放内存空间
二. 线程池基础
23. 为什么要用线程池?
- 降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗
- 提高响应速度。 当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
24. 执行execute()方法和submit()方法的区别是什么呢?
- execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
- submit()方法用于提交需要返回值的任务。
25. 线程池的主要核心原理?
- 创建一个空的线程池
- 提交任务时,线程池会创建新的线程对象,任务执行完毕,线程放入线程池中,下次在提交任务时,不需要创建新的线程,直接用已有的线程
- 但是如果任务提交时,池子中没有空闲线程,也无法创建新的线程,任务就会排队等待
26. 常用的JAVA线程池有哪几种类型?
- newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
- newFixedThreadPool:创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中
- newSingleThreadExecutor:创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序 (优先级)执行
- newScheduleThreadPool:创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行
27. 说下自定义线程池核心参数?
- corePoolSize : 核心线程数量,不能小于0
- maximumPoolSize :最大线程数量,且要大于等于核心线程数量
- keepAliveTime :空闲线程最大存活时间
- (TimeUnit.SECONDS): // 空闲线程的时间单位
- workQueue :任务队列
- ThreadFactory :线程工厂
- defaultHandler :任务的拒绝策略
- AbortPolicy : 线程任务丢弃报异常。默认拒绝策略。
28. 自定义线程池执行任务的流程?
- 创建一个空的线程池,有任务提交时,线程池会创建线程去执行任务,执行完毕归还线程
- 当核心线程满时,在提交任务就会排队
- 当核心线程满且任务队列满时,会创建临时线程
- 当核心线程、任务队列和临时现场都满时,会触发任务拒绝策略
29. Executor和Executors的区别?
- Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。
- Executor 接口对象能执行我们的线程任务。ExecutorService接口继承了Executor接口并进行了扩展,提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值。
三. AQS
30. 说一说什么是AQS?
- AQS 是一个锁框架,它定义了锁的实现机制,并开放出扩展的地方,让子类去实现
- AQS 底层是由同步队列 + 条件队列联手组成,同步队列管理着获取不到锁的线程的排队和释放,条件队列是在一定场景下,对同步队列的补充
- AQS 围绕两个队列,提供了四大场景,分别是:获得锁、释放锁、条件队列的阻塞,条件队列的唤醒,分别对应着 AQS 架构图中的四种颜色的线的走向。
四. Atomic原子性
31. 介绍一下 Atomic 原子类
- Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
- 并发包 java.util.concurrent 的原子类都存放在 java.util.concurrent.atomic 下:
32. JUC 包中的原子类是哪4类?
- 基本类型
- AtomicInteger : 整型原子类;AtomicLong: 长整型原子类;AtomicBoolean: 布尔型原子类
- 数组类型
- AtomicIntegerArray: 整型数组原子类;AtomicLongArray: 长整型数组原子类;AtomicReferenceArray: 引用类型数组原子类
- 引用类型
- AtomicReference: 引用类型原子类;AtomicStampedReference: 原子更新带有版本号的引用类型;AtomicMarkableReference: 原子更新带有标记位的引用类型;AtomicIntegerFieldUpdater: 原子更新整型字段的更新器;AtomicLongFieldUpdater: 原子更新长整型字段的更新器;AtomicMarkableReference: 原子更新带有标记位的引用类型
33. 简单介绍一下 AtomicInteger 类的原理
- AtomicInteger 类主要利用 CAS和 volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。