Java并发面试题总结
发布日期:2021-05-07 13:00:46 浏览次数:31 分类:精选文章

本文共 4084 字,大约阅读时间需要 13 分钟。

前言

大家好,我是孤焰。我是一名大三在校大学生,目前也在准备春招实习的事情,在复习的过程中整理出一些笔记,现在分享给大家,希望大家可以一起进步,哪里还有不足的地方希望大家指出,我会积极改进的。


简述线程、程序、进程的基本概念。以及他们之间关系是什么?

  • 线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

  • 程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。

  • 进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。

线程有哪些基本状态?(重点)

在这里插入图片描述

从 JVM 角度说进程和线程之间的关系

一个进程中可以有多个线程,多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器虚拟机栈本地方法栈

总结: 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。

说说并发与并行的区别?

  • 并发: 同一时间段,多个任务都在执行 (单位时间内不一定同时执行);
  • 并行: 单位时间内,多个任务同时执行。

使用多线程可能带来什么问题?

内存泄漏、死锁、线程不安全等

什么是上下文切换?

概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换

如何避免线程死锁?(重点)

  • 破坏互斥条件 :这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
  • 破坏请求与保持条件 :一次性申请所有的资源。
  • 破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  • 破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

说说 sleep() 方法和 wait() 方法区别和共同点?

两者最主要的区别在于:sleep() 方法没有释放锁,而 wait() 方法释放了锁 。(重点)

两者都可以暂停线程的执行。

wait() 通常被用于线程间交互/通信,sleep() 通常被用于暂停执行。

wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout) 超时后线程会自动苏醒。

为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?(重点)

调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。

说说 JDK1.6 之后的 synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗?

JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。

锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

谈谈 synchronized 和 ReentrantLock 的区别?

  • 两者都是可重入锁

  • synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API

  • ReentrantLock 比 synchronized 增加了一些高级功能:

    • 等待可中断:ReentrantLock提供了一种能够中断等待锁的线程的机制,通过 lock.lockInterruptibly() 来实现这个机制。
    • 可实现公平锁:ReentrantLock可以指定是公平锁还是非公平锁。
    • 可实现选择性通知:synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制。

讲一下 JMM(Java 内存模型)?

在 JDK1.2 之前,Java 的内存模型实现总是从主存(即共享内存)读取变量,是不需要进行特别的注意的。

而在当前的 Java 内存模型下,线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。

这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-olFlEjYJ-1612270645545)(C:\Users\14842\AppData\Roaming\Typora\typora-user-images\image-20210202204258643.png)]

要解决这个问题,就需要把变量声明为volatile,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。

所以,volatile 关键字 除了防止 JVM 的指令重排 ,还有一个重要的作用就是保证变量的可见性。

并发编程的三个重要特性?(重点)

  • 原子性
  • 可见性
  • 有序性

ThreadLocal 内存泄露问题?

ThreadLocalMap中使用的 key 为 ThreadLocal弱引用,而 value 是强引用。

所以,如果 ThreadLocal没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap中就会出现 key 为 null 的 Entry。

假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。

ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove()方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法

实现 Runnable 接口和 Callable 接口的区别(重点)

Runnable自 Java 1.0 以来一直存在,但Callable仅在 Java 1.5 中引入,目的就是为了来处理Runnable不支持的用例。Runnable 接口不会返回结果或抛出检查异常,但是Callable 接口可以。所以,如果任务不需要返回结果或抛出异常推荐使用 Runnable 接口,这样代码看起来会更加简洁。

工具类 Executors 可以实现 Runnable 对象和 Callable 对象之间的相互转换。(Executors.callable(Runnable task)或 Executors.callable(Runnable task,Object resule))。

执行 execute()方法和 submit()方法的区别是什么呢?(重点)

  • execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;

  • submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Future 的 get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

ThreadPoolExecutor`构造函数重要参数分析(重点)

ThreadPoolExecutor 3 个最重要的参数:

  1. corePoolSize : 核心线程数线程数定义了最小可以同时运行的线程数量。
  2. maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
  3. workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。

ThreadPoolExecutor其他常见参数:

  1. keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
  2. unit : keepAliveTime 参数的时间单位。
  3. threadFactory :executor 创建新线程的时候会用到。
  4. handler :饱和策略。

JUC 包中的原子类是哪 4 类?(重点)

在这里插入图片描述

什么是悲观锁?(重点)

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。

Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。

什么是乐观锁?(重点)

总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量。

在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

两种锁的使用场景?(重点)

乐观锁适用于写比较少的情况下(多读场景)。

悲观锁适用于多写的场景。

乐观锁的缺点?(重点)

  • ABA问题
  • 循环时间长开销大
  • 只能保证一个共享变量的原子操作

AQS组件总结

在这里插入图片描述

上一篇:如何完美解答面试问题——请简述一下垃圾回收机制?
下一篇:【读书笔记】《深入理解Java虚拟机》对象的访问定位

发表评论

最新留言

哈哈,博客排版真的漂亮呢~
[***.90.31.176]2025年03月29日 17时06分10秒