作者有话说:目前正在跟新一系列的java面试题,持续不断更新。需要找工作或者不需要找工作的猴子们,都可以关注一下。着急的可以评论区留留言,面试文档以及简历模板。看到了我会发
1. 说说进程和线程的区别
进程
当一个程序在计算机上运行时,通常会创建至少一个进程。进程被认为是操作系统分配资源的最小单元,每个进程都拥有独立的内存空间和系统资源,
例如,你浏览器、文本编辑器、音乐播放器等多个进程。
线程
每个进程至少包含一个线程,在一个进程内,多个线程可以协同工作,执行不同的任务,共享数据。这种多线程的使用方式有助于提高程序的并发性和性能
例如,一个文字处理软件的进程可能包括一个主线程,用于处理用户界面响应,同时还有一个后台线程,负责自动保存文件。
2. 说说并发和并行的区别
- 并发:指在一段时间内,多个任务交替执行。系统会在不同的时间点上切换执行不同的任务。即使只有一个处理器核心,也可以实现并发。通过线程的快速切换,使得在宏观上看起来多个任务在同时进行。
- 并行:指多个任务在同一时刻同时执行。这通常需要多个处理器核心或多核 CPU 来支持。
单核 CPU 只能并发,无法并行;换句话说,并行只可能发生在多核 CPU 中。在多核 CPU 中,并发和并行通常会同时存在。多个任务可以在不同的核心上并行执行
区别:
- 资源利用:并行可以更好地利用多核处理器的资源,提高系统的性能。
- 执行效率:并行在处理大量计算任务时效率更高。
- 线程交互:并发中的线程可能会相互竞争资源,需要处理线程间的同步和互斥问题。
3. 并发、并行、串行之间的区别
- 串行:一个任务执行完,才能执行下一个任务
- 并行(Parallelism):两个任务同时执行
- 并发(Concurrency):两个任务整体看上去是同时执行,在底层,两个任务被拆成了很多份,然后一个一个执行,站在更高的角度看来两个任务是同时在执行的
4. Java线程之间是如何通信的
当我们处理线程通信时,通常有两种主要的实现方式
1. 共享内存: 这是一种常见的方式,多个线程可以访问同一个共享内存区域,通过读取和写入共享内存中的数据来进行通信和同步
2.消息传递: 另一种方式是消息传递,多个线程之间通过消息队列、管道、信号量等机制来传递信息和同步状态。
5. JAVA 中用到的线程调度算法是什么
在Java中,线程调度采用的是一种抢占式调度模型。这就像在一个抢夺战中,有较高优先级的线程将首先占用CPU资源。如果线程具有相同的优先级,那么Java虚拟机会随机选择一个线程来执行,以保持公平竞争的原则。一旦一个线程获得了CPU,它将一直运行,直到自愿放弃CPU资源,或者由于某些情况(比如等待I/O操作、等待锁等)被迫放弃CPU,从而让其他线程有机会执行
6. JAVA 守护线程和本地线程的区别
守护线程,
可以看作是为其他线程提供服务的辅助工具。它们通常执行一些后台任务,比如垃圾回收、定期检查、日志记录等。一个重要的特点是它们不会阻止JVM的退出。当所有的本地线程都执行完毕后,JVM会自动退出,不会等待守护线程。守护线程需要通过设置 setDaemon(true) 来告诉JVM它是守护线程
本地线程,
则是应用程序的主力军,执行着应用的核心逻辑。它们的存在会阻止JVM退出,因为只要还有本地线程在运行,JVM会继续工作。本地线程则是默认的线程类型,不需要额外设置。
7. 启动线程为何调用start而不是run方法
调用 start() 方法会告诉jvm创建一个新的线程,并在这个新线程中执行与 run() 方法相关联的代码块。这个过程允许多个线程同时运行。
如果直接调用 run() 方法,它仅仅是一个普通的方法调用,不会创建新的线程
8. 死锁与活锁,死锁与饥饿的区别
死锁
是指多个线程相互等待对方释放资源,导致它们都无法继续执行下去。这是一种静止状态,这种情况会导致所有线程都被永久性地阻塞,没有一个线程能够继续执行
活锁
是指多个线程不断地改变自己的状态以回应对方,但最终无法取得进展,导致线程不断重试相同的操作,却无法成功
饥饿
是指一个或多个线程或进程由于某种原因无法获得所需的资源或执行机会,因此无法适时地执行。这是一种动态问题,通常由资源分配不合理或线程优先级设置不当等原因导致
9. 如何避免死锁
死锁是多线程编程中的一种常见问题,它发生在两个或多个线程相互等待对方释放资源的情况下,导致程序无法继续执行。为了避免死锁
- 锁顺序: 定义一个固定的锁获取顺序,并要求所有线程都按照相同的顺序获取锁。这可以减少不同线程之间资源争夺的可能性。
- 超时机制: 在获取锁时,设置一个超时时间。如果超过指定时间仍然无法获取锁,线程应该释放已经持有的锁并重试,或者采取其他适当的措施。这有助于避免线程无限期地等待锁。
- 避免嵌套锁: 尽量避免在一个锁的持有期间再次尝试获取其他锁。如果确实需要获取多个锁,请确保获取的顺序是固定的,以减少死锁风险。
- 使用锁机制: 比如Java中的ReentrantLock,它支持可中断的锁获取和条件等待,有助于避免死锁。
10. 什么情况线程会进入 WAITING 状态
当线程进入WAITING状态时,会释放所有的锁,并且不会占用CPU资源。
- 调用Object.wait()方法,该方法会使得当前线程进入等待状态,等待其他线程调用同一个对象的notify()或notifyAll()方法唤醒该线程。
- 调用Thread.join()方法,该方法会使得当前线程等待指定线程的结束,当指定线程结束时,当前线程将被唤醒。
- 调用LockSupport.park()方法,该方法会使得当前线程等待,直到获取LockSupport指定的许可或者线程被中断、调度。
- 在获取锁时,如果当前线程没有获取到锁,就会进入等待状态,等待其他线程释放锁。
- 等待I/O操作:当线程在执行期间遇到了需要等待I/O操作完成的情况,它会进入WAITING状态,直到I/O操作完成。
11.说说synchronized与ReentrantLock的区别
- sychronized是一个关键字,是在JVM层面通过监视器实现的; ReentrantLock是一个类,基于AQS实现的。
- synchronized 是隐式锁,会自动的加锁与释放锁,ReentrantLock需要程序员手动加锁与释放锁
- sychronized的底层是JVM层面的锁,ReentrantLock是API层面的锁
- sychronized是非公平锁,ReentrantLock可以选择公平锁或非公平锁
- sychronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过代码中int类型的state标识来标识锁的状态
- sychronized底层有一个锁升级的过程
- synchronized 可以用于修饰普通方法、静态方法以及代码块,而 ReentrantLock 仅适用于代码块。
12. Sychronized的偏向锁、轻量级锁、重量级锁
- 偏向锁:在锁对象的对象头中记录一下当前获取到该锁的线程ID,该线程下次如果又来获取该锁就可以直接获取到了
- 轻量级锁:由偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁,此时如果有第二个线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻量级锁底层是通过自旋来实现的,并不会阻塞线程
- 如果自旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞
- 自旋锁:自旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就无所谓唤醒线程,阻塞和唤醒这两个步骤都是需要操作系统去进行的,比较消耗时间,自旋锁是线程通过CAS获取预期的一个标记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程一直在运行中,相对而言没有使用太多的操作系统资源,比较轻量。
13. ReentrantLock中的公平锁和非公平锁的底层实现
ReentrantLock是Java中提供的一种可重入锁,它支持两种锁的模式:公平锁和非公平锁
- 公平锁(Fair Lock): 公平锁的特点是按照请求锁的顺序来分配锁,即先到先得。在ReentrantLock中,通过构造函数可以选择创建一个公平锁。公平锁的底层实现使用了一个FIFO队列(First-In-First-Out),即等待队列。当一个线程请求锁时,如果锁已经被其他线程持有,请求线程会被放入等待队列的末尾,按照请求的顺序等待锁的释放。当锁被释放时,等待队列中的第一个线程会被唤醒并获得锁。
- 非公平锁(Non-Fair Lock): 非公平锁不考虑请求锁的顺序,它允许新的请求线程插队并尝试立即获取锁,而不管其他线程是否在等待。在ReentrantLock中,默认情况下创建的是非公平锁。非公平锁的底层实现中,有一个等待队列,但它不会严格按照请求的顺序来分配锁,而是根据线程竞争锁的情况来判断是否立即分配给新的请求线程。
15.ReentrantLock中tryLock()和lock()方法的区别
- tryLock()表示尝试加锁,可能加到,也可能加不到,该方法不会阻塞线程,如果加到锁则返回true,没有加到则返回false
- lock()表示阻塞加锁,线程会阻塞直到加到锁,方法也没有返回值
说说ThreadLocal原理
ThreadLocal 是 Java 中的一个类,用于实现线程封闭(Thread-local)的数据存储机制。每个线程都有自己的 ThreadLocalMap,其中 ThreadLocal 对象充当键,线程的变量副本作为对应键的值。使用 ThreadLocal 的 set() 方法将数据存储在当前线程的 ThreadLocalMap 中,而使用 get() 方法则是从当前线程的 ThreadLocalMap 中获取数据副本。
这种机制为每个线程维护独立的变量副本,这样可以实现线程之间的数据隔离,从而有效地避免了线程安全问题。这对于需要在线程内部存储线程特定数据的情况非常有用,例如数据库连接、用户会话信息等。但需要谨慎使用 ThreadLocal,确保在不再需要时及时清理变量副本,以避免潜在的内存泄漏问题。这种机制提供了一种有效的方式来确保线程级别的数据隔离和线程安全性。
15. ThreadLocaL如何防止内存泄漏
ThreadLocal 变量的内存泄漏问题主要是由于 ThreadLocalMap 中的 Entry 没有被及时清理导致的。ThreadLocalMap 是 ThreadLocal 的底层数据结构,它用于存储每个线程独立的变量副本。可以考虑以下方法:
- 使用完 ThreadLocal 后及时调用 remove() 方法:在不再需要使用 ThreadLocal 存储的数据时,手动调用 ThreadLocal.remove() 方法将该数据从当前线程的 ThreadLocalMap 中清除。这样可以确保 ThreadLocalMap 不会持有对对象的引用,从而帮助垃圾回收器正常回收不再需要的对象。
- 使用 try-with-resources 或 try-finally 块:如果你的 ThreadLocal 变量在需要清理的资源管理上下文中使用,可以使用 try-with-resources(自动清理)或 try-finally(手动清理)块来确保及时清理。
- 使用 InheritableThreadLocal:如果需要在子线程中访问父线程的 ThreadLocal 变量,并且确保在子线程中正确清理,可以考虑使用 InheritableThreadLocal。这个类允许子线程继承父线程的 ThreadLocal 变量,并在子线程完成后自动清理。
16. 如何确保线程安全
- synchronized关键字:synchronized关键字可以确保同一时刻只有一个线程可以执行某个代码块,从而避免了多个线程同时访问和修改共享资源的问题。
- ReentrantLock类:ReentrantLock类是Java提供的一种可重入锁,与synchronized关键字类似,但它提供了更多的灵活性和功能。
- 线程安全的数据结构:Java提供了多种线程安全的数据结构,如ConcurrentHashMap、CopyOnWriteArrayList等,这些数据结构内部已经实现了线程安全,可以直接使用。
- 线程池:线程池可以避免创建和销毁线程的开销,并且可以有效地控制并发量,保证线程安全。
- 避免共享状态: 如果可能,尽量避免多个线程共享状态。将数据封装在线程内部,减少共享数据的需求。
- 使用线程安全的设计模式: 了解并应用线程安全的设计模式,如单例模式中的双重检查锁定等。
17. 什么是可重入锁
可重入锁可以简单理解为一个可以重复获取的锁,就像拿钥匙开锁一样,你可以反复用同一把钥匙开锁。这种锁在同一线程内是安全的,因为它可以被同一线程多次获取,而不会产生不一致的状态。可重入锁是一种安全的锁机制,可以避免死锁的发生。
18. 什么是不可变对象,对写并发有什么帮助
不可变对象(Immutable object)是一种一旦创建后其状态就不能被修改的对象。在Java中,不可变对象包括String、基本类型的包装类(如Integer、Double等)等。
- 线程安全:不可变对象是线程安全的,因为它们不会被其他线程修改。因此,多个线程可以同时使用不可变对象,无需额外的同步措施。
- 减少锁竞争:由于不可变对象的状态不能被修改,因此不需要使用锁来保护对它的访问。这减少了锁竞争的可能性,从而提高了程序的性能。
- 缓存优化:由于不可变对象一旦创建后其状态就不能被修改,因此可以将它们用作缓存项。这是因为缓存项的值不会在缓存和使用之间发生改变,从而避免了因缓存项状态被修改而导致的缓存失效问题。
19. 说说你对JMM内存模型的理解
Java内存模型(JMM)是一种抽象的概念,用于描述在Java程序中,一组线程如何通过共享内存进行交互。JMM并不真实存在,它是一种规范
在JMM中,内存主要划分为两种类型:主内存和工作内存。主内存存储了所有的对象实例和静态变量,而工作内存存储了每个线程的局部变量、栈中的部分区域以及寄存器的内容。
JMM定义了一系列规则来规范线程之间的内存访问。其中最重要的规则是:当一个线程想要访问一个共享变量的值时,它必须先将其本地内存中的值更新到主内存中,然后再从主内存中读取该变量的值。这个过程被称为“主内存的一致性”。
JMM的作用主要是屏蔽底层硬件和操作系统的内存访问差异,实现平台一致性
20. 说下对AQS的理解
AQS(AbstractQueuedSynchronizer)是Java并发编程中的一个重要组件,它是一个抽象类。提供了线程同步的底层实现机制。AQS的作用是实现线程的同步和互斥操作,它提供了两种主要的锁机制,分别是排他锁和共享锁。
排他锁:也称为独占锁,在多个线程竞争同一共享资源时,同一时刻只允许一个线程访问该共享资源,即多个线程中只有一个线程获得锁资源。在AQS中,排他锁是通过内置的同步状态来实现的。当同步状态为0时,表示锁是未被获取的;当同步状态大于0时,表示锁已经被获取且被占用;当同步状态小于0时,表示锁已经被获取但是处于等待状态。
共享锁:允许多个线程同时获得锁资源,但是在同一时刻只有一个线程可以获取到锁的拥有权,其他线程需要等待该线程释放锁。在AQS中,共享锁的实现与排他锁类似,也是通过内置的同步状态来实现的。
在实现AQS时,需要继承自AQS类并实现其抽象方法。其中比较重要的方法包括:tryAcquire()和tryRelease()方法,用于实现锁的获取和释放;acquire()和release()方法,用于实现阻塞和唤醒操作;isHeldExclusively()方法,用于判断是否是排他锁。
21. 说下CAS的原理
CAS(Compare And Swap)是一种乐观的并发控制机制,它的核心原理是基于硬件层面的原子性保证。CAS操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。它的工作原理是:
- 在将新值写入内存之前,CAS操作会先比较内存位置的值是否与预期原值相匹配。
- 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置的值更新为新值。
- 如果内存位置的值与预期原值不匹配,则CAS操作失败,不会修改内存值。
CAS的优势在于它没有阻塞状态,不会引起线程上下文的切换和调度问题
22. 创建线程的几种方法
- 继承Thread类: 创建一个继承自Thread类的子类,然后重写子类的run方法,将线程的任务逻辑放在run方法中。然后通过创建子类的对象并调用start方法来启动线程
- 实现Runnable接口: 创建一个类实现Runnable接口,并重写run方法,然后通过将实现了Runnable接口的对象传递给Thread类的构造函数来创建线程对象。
- 使用线程池(Executor框架): 可以使用Executor框架中的线程池来管理和执行线程。通过ExecutorService接口的实现类,例如ThreadPoolExecutor,可以提交任务并由线程池管理线程的生命周期。
23. 说下你对volatile的理解
- 保证可见性:volatile保证了多个线程对共享变量的操作是可见的。当一个线程修改了共享变量的值,其他线程会立即看到这个改变。
- 禁止指令重排:volatile通过禁止指令重排来保证顺序性。在多线程环境下,为了提高程序执行效率,编译器和处理器可能会对指令进行重新排序。但是,如果一个变量被volatile修饰,就禁止了指令重排,确保每个线程都能看到正确的操作顺序。
volatile可以确保多个线程对共享变量的操作一致,避免了数据不一致的问题。但是它不能保证原子性
24. JDK7与JDK8之间HashMap的区别
- JDK8中新增了红黑树,JDK8是通过数组+链表+红黑树来实现的;
- JDK7中链表的插入是用的头插法,而JDK8中则改为了尾插法;
- JDK8中的因为使用了红黑树保证了插入和查询了效率,所以实际上JDK8中的Hash算法实现的复杂度降低了;
- JDK8中数组扩容的条件也发了变化,只会判断是否当前元素个数是否查过了阈值,而不再判断当前put进来的元素对应的数组下标位置是否有值;
- JDK7中是先扩容再添加新元素,JDK8中是先添加新元素然后再扩容。
25. 说下ConcurrentHashMap和Hashtable的异同点
ConcurrentHashMap 和 Hashtable 都是用于在多线程环境中存储和操作键值对的数据结构
相似点:
- 线程安全性:ConcurrentHashMap 和 Hashtable 都是线程安全的,可以在多个线程同时访问它们而不需要额外的同步措施。
- 键值对存储: 两者都以键值对的方式存储数据,允许通过键来查找和访问值。
不同点:
同步策略:
- ConcurrentHashMap 使用了更加精细的分段锁(Segment),每个分段可以看作一个小的独立的哈希表,不同分段之间可以并发操作,因此多线程访问不同分段的数据时不会阻塞。这使得在多线程环境中,ConcurrentHashMap 的性能更好,因为只有在访问相同分段的数据时才需要竞争锁。
- Hashtable 使用了全局锁,也就是在任何时刻只能有一个线程访问 Hashtable 的数据,这导致了在高并发环境下性能较差。
Null 键和值
- ConcurrentHashMap 不允许存储 null 键或 null 值。如果尝试存储 null 键或值,会抛出 NullPointerException。
- Hashtable 不允许存储 null 键和值。
.性能
- ConcurrentHashMap 通常在高并发环境下性能更好,因为它使用了分段锁,允许多个线程同时读取和写入不同分段的数据。
- Hashtable 的性能相对较差,因为它使用全局锁,只允许一个线程操作整个数据结构。
总的来说,ConcurrentHashMap 是在多线程环境中更常用的选择,因为它提供了更好的性能和灵活性,同时避免了 Hashtable 中的一些限制和性能瓶颈
26. 线程池的底层工作原理
其底层工作原理涉及线程的创建、调度、执行以及回收等关键过程。
- 线程池的创建:通过配置参数创建线程池,如核心线程数、最大线程数等。
- 任务队列:用于存储待执行的任务。
- 工作线程:线程池中的线程,执行任务队列中的任务。
- 任务提交:将任务提交到线程池。
- 任务调度:
- 如果有空闲的工作线程,直接分配任务给它执行。
- 如果没有空闲线程,任务被放入任务队列等待执行。
- 线程创建:
- 当任务队列满且线程数小于最大线程数时,创建新的工作线程。
- 线程回收:
- 当线程空闲一段时间后,可能会被回收以节省资源。
- 异常处理:线程池会处理任务执行过程中的异常。
线程池内部是通过队列+线程实现的,当我们利用线程池执行任务时:
- 如果此时线程池中的线程数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
- 如果此时线程池中的线程数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列。
- 如果此时线程池中的线程数量大于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。
- 如果此时线程池中的线程数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。
- 当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数
27. 线程池为什么是先添加列队而不是先创建最大线程?
当线程池中的核心线程都在忙时,如果继续往线程池中添加任务,那么任务会先放入队列,队列满了之后,才会新开线程。这就相当于,一个公司本来有10个程序员,本来这10个程序员能正常的处理各种需求,但是随着公司的发展,需求在慢慢的增加,但是一开始这些需求只会增加在待开发列表中,然后这10个程序员加班加点的从待开发列表中获取需求并进行处理,但是某一天待开发列表满了,公司发现现有的10个程序员是真的处理不过来了,所以就开始新招员工了。
28. 线程池的哪些参数影响性能
- 核心线程数(corePoolSize): 这是线程池中一直保持活动状态的最小线程数量。核心线程在空闲时不会被销毁,除非启用了 allowCoreThreadTimeOut 选项。核心线程数的设置直接影响了线程池的基本并发度。
- 最大线程数(maximumPoolSize): 当任务提交到线程池时,首先会尝试使用已有的空闲线程来处理,如果没有空闲线程则根据需要创建新线程。最大线程数限制了线程池可以创建的最大线程数量,过高的设置可能会导致资源消耗过多。
- keepAliveTime 和 TimeUnit: 这两个参数用于控制空闲线程的存活时间。如果线程在空闲时间超过 keepAliveTime 指定的时间段,它将被终止并从线程池中移除。合理的设置可以降低线程池的维护成本。
- 工作队列(workQueue): 工作队列用于存储等待执行的任务。不同类型的工作队列(如有界队列或无界队列)对线程池的性能有一定影响。有界队列可以避免无限制的任务积压,但可能导致任务丢失,而无界队列可能会占用更多的内存。
29. 线程池中线程复用原理
线程池的线程复用原理是指,将线程放入线程池中重复利用,而不是每执行一个任务就创建一个新线程。线程池会对线程进行封装,核心原理在于将线程的创建和管理与任务的执行分离。
当提交任务时,线程池首先会检查当前线程数是否小于核心线程数,如果是,则新建一个线程来执行任务;如果当前线程数已经达到核心线程数,但队列中没有正在执行的任务,则将任务放入队列中等待执行;如果队列已满,且线程池中的线程数量未达到最大线程数,则新建线程来执行任务;如果队列已满,且线程池中的线程数量达到最大线程数,则根据拒绝策略来处理无法执行的任务。
30.如何优化线程池的性能
要优化线程池的性能,需要根据实际情况进行参数配置。以下是一些优化建议:
- 根据应用场景和任务性质,合理设置核心线程数(corePoolSize)和最大线程数(maximumPoolSize)。如果任务主要是IO密集型的,核心线程数可以设置为CPU核心数的两倍左右,最大线程数可以设置为CPU核心数的四倍左右。如果任务是CPU密集型的,核心线程数可以适当减少,最大线程数也可以适当减少。
- 根据任务性质和实际需求,选择合适的任务队列(workQueue)。例如,如果任务是CPU密集型的,可以选择容量较大的有界队列,以减少线程的创建和销毁;如果任务是I/O密集型的,可以选择容量较小的无界队列,以避免队列过小导致的任务拒绝问题。
- 根据系统资源和任务性质,合理设置线程的存活时间(keepAliveTime)。如果系统资源充足且任务性质不紧张,可以适当增加线程存活时间,以减少线程的创建和销毁;如果系统资源有限或任务性质较为紧张,可以适当减少线程存活时间,以减少线程的空闲时间。
- 根据实际需求,自定义线程工厂(threadFactory)和任务拒绝策略(handler)。可以通过实现自己的线程工厂来设置线程的名称、优先级等属性,以提高线程池的可维护性。当任务队列已满且线程数达到最大值时,可以使用任务拒绝策略来处理无法执行的任务,例如抛出异常、记录日志或尝试重新提交等。
.