7. Java 中的 final 关键字是否能保证变量的可见性?
可见性定义
: 一般而言指的是一个线程修改了共享变量,另一个线程可以立马得知更改,得到最新修改后的值。
final 并不能保证这种情况的发生,volatile 才可以。
final的可见性指的是:
- final 修饰的字段在构造方法初始化完成,并且期间没有把 this 传递出去,那么当构造器执行完毕之后,其他线程就能看见 final 字段的值。
- 如果不用 final 修饰的话,那么有可能在构造函数里面对字段的写操作被排序到外部,这样别的线程就拿不到写操作之后的值。
8. 什么是 Java 内存模型(JMM)?
Java 内存模型(Java Memory Model, JMM) 是 Java 虚拟机 (JVM) 定义的一种
规范
,用于描述多线程程序中变量(包括实例字段、静态字段和数组元素)如何在内存中存储和传递的规则。规范了线程何时会从主内存中读取数据、何时会把数据写回主内存。
目的:
- 可见性:确保一个线程对变量的修改,能及时被其他线程看到。关键字 volatile 就是用来保证可见性的,它强制线程每次读写时都直接从主内存中获取最新值。
- 有序性:指线程执行操作的顺序。JMM 允许某些指令重排序以提高性能,但会保证线程内的操作顺序不会被破坏,并通过 happens-before 关系保证跨线程的有序性。
- 原子性:是指操作不可分割,线程不会在执行过程中被中断。例如,synchronized 关键字能确保方法或代码块的原子性。
9. 什么是 Java 的 CAS(Compare-And-Swap)操作?
CAS 是一种硬件级别的原子操作,它比较内存中的某个值是否为预期值,如果是,则更新为新值,否则不做修改。
- 工作原理:
期望值, 内存地址, 新值
- 比较(Compare):CAS 会检查内存中的某个值是否与预期值相等。
- 交换(Swap):如果相等,则将内存中的值更新为新值。
- 失败重试:如果不相等,说明有其他线程已经修改了该值,CAS 操作失败,一般会利用重试,直到成功。
- 优点:
- 无锁并发:CAS 操作不使用锁,因此不会导致线程阻塞,提高了系统的并发性和性能。
- 原子性:CAS 操作是原子的,保证了线程安全。
- 缺点:
- ABA 问题:CAS 操作中,如果一个变量值从 A 变成 B,又变回 A,CAS 无法检测到这种变化,可能导致错误。
- 单变量限制:CAS 操作仅适用于单个变量的更新,不适用于涉及多个变量的复杂操作。
- 自旋开销:CAS 操作通常通过自旋实现,可能导致 CPU 资源浪费,尤其在高并发情况下。
10. 什么是 Java 中的 ABA 问题?
ABA 问题是指在多线程环境下,某个变量的值在一段时间内经历了从 A 到 B 再到 A 的变化,这种变化可能被线程误认为值没有变化,从而导致错误的判断和操作。ABA 问题常发生在使用 CAS(Compare-And-Swap) 操作的无锁并发编程中。
解决方法:
- AtomicStampedReference: 增加
int
的版本号 - AtomicMarkableReference: 增加
boolean
的版本号
11. 为什么 Java 中的 ThreadLocal 对 key 的引用为弱引用?
使用弱引用作为 ThreadLocal 的键可以防止内存泄漏。
- 若 ThreadLocal 实例被不再需要的线程持有为强引用,那么当该线程结束时,相关的 ThreadLocal 实例及其对应的数据可能无法被回收,导致内存持续占用。
- 若弱引用允许垃圾回收器在内存不足时回收对象。这样,当没有其他强引用指向某个 ThreadLocal 实例时,它可以被及时回收,避免长时间占用内存。
12. 你了解 Java 线程池的原理吗?
线程池是一种池化技术,用于预先创建并管理一组线程,避免频繁创建和销毁线程的开销,提高性能和响应速度。
- ThreadPoolExecutor关键参数
- 核心线程数coreSize
- 最大线程数maximumPollSize
- 空闲存活时间keepAliveTime
- 时间单位TimeUnit
- 工作队列workQueue
- SynchronousQueue:不存储任务,直接将任务提交给线程。
- LinkedBlockingQueue:链表结构的阻塞队列,大小无限。
- ArrayBlockingQueue:数组结构的有界阻塞队列。
- PriorityBlockingQueue:带优先级的无界阻塞队列。
- 线程工厂ThreadFactory
- 拒绝策略RejectFactoryHandler
- AbortPolicy,当任务队列满且没有线程空闲,此时添加任务会直接抛出 RejectedExecutionException 错误,这也是默认的拒绝策略。适用于必须通知调用者任务未能被执行的场景。
- CallerRunsPolicy,当任务队列满且没有线程空闲,此时添加任务由即调用者线程执行。适用于希望通过减缓任务提交速度来稳定系统的场景。
- DiscardOldestPolicy,当任务队列满且没有线程空闲,会删除最早的任务,然后重新提交当前任务。适用于希望丢弃最旧的任务以保证新的重要任务能够被处理的场景。
- DiscardPolicy,直接丢弃当前提交的任务,不会执行任何操作,也不会抛出异常。适用于对部分任务丢弃没有影响的场景,或系统负载较高时不需要处理所有任务。
- 工作原理
- 默认情况下线程不会预创建,任务提交之后才会创建
核心线程
(不过设置 prestartAllCoreThreads 可以预创建核心线程)。 - 当核心线程满了之后不会新建线程,而是把任务堆积到
工作队列
中。 - 如果工作队列放不下了,然后才会新增线程,直至达到
最大线程
数。 - 如果工作队列满了,然后也已经达到最大线程数了,这时候来任务会执行
拒绝策略
。 - 当线程
空闲时间
超过空闲存活时间,并且线程线程数是大于核心线程数的则会销毁线程,直到线程数等于核心线程数(设置 allowCoreThreadTimeOut 为 true 可以回收核心线程,默认为 false)。
- 默认情况下线程不会预创建,任务提交之后才会创建
13. 你使用过哪些 Java 并发工具类?
- ConcurrentHashMap
- 是一个线程安全且高效的哈希表,支持并发访问。
- 多个线程可以同时进行读写操作,而不会导致线程安全问题。
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key1", 1);
Integer value = map.get("key1");
map.computeIfAbsent("key2", k -> 2);
- AtomicInteger
- 提供一种线程安全的方式对 int 类型进行原子操作,如增减、比较。
- 适用于需要频繁对数值进行无锁操作的场景。
AtomicInteger atomicInt = new AtomicInteger(0);
atomicInt.incrementAndGet(); // 递增
atomicInt.decrementAndGet(); // 递减
atomicInt.compareAndSet(1, 2); // 比较并设置
- BlockingQueue
- 提供一种线程安全的方式对 int 类型进行原子操作,如增减、比较。
- 适用于需要频繁对数值进行无锁操作的场景。
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
Runnable producer = () -> {
try {
queue.put("item"); // 放入元素
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Runnable consumer = () -> {
try {
String item = queue.take(); // 取出元素
} catch (InterruptedException e) {
e.printStackTrace();
}
};
new Thread(producer).start();
new Thread(consumer).start();
- CompletableFuture
- 一个线程可以异步执行任务,并在任务完成后通知其他线程。
- 适用于需要异步处理任务并等待结果的场景,可以组合多个异步任务并处理它们的最终结果。
// 创建一个 CompletableFuture 任务
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// 模拟异步任务
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Hello, CompletableFuture!";
});
// 主线程可以继续执行其他任务
System.out.println("主线程继续执行...");
// 等待异步任务完成并获取结果
String result = future.get(); // 阻塞等待任务完成
System.out.println("异步任务结果: " + result);
- Semaphore
- 控制访问资源的线程数,可以用来实现限流或访问控制。
- 在资源有限的情况下,控制同时访问的线程数量。
Semaphore semaphore = new Semaphore(3);
try {
semaphore.acquire(); // 获取许可
// 执行任务
} finally {
semaphore.release(); // 释放许可
}
- CyclicBarrier
- 让一组线程到达一个共同的同步点,然后一起继续执行。常用于分阶段任务执行。
- 适用于需要所有线程在某个点都完成后再继续的场景。
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("所有线程都到达了屏障点");
});
Runnable task = () -> {
try {
// 执行任务
barrier.await(); // 等待其他线程
} catch (Exception e) {
e.printStackTrace();
}
};
new Thread(task).start();
new Thread(task).start();
new Thread(task).start();
- CountDownLatch
- 一个线程(或多个)等待其他线程完成操作。
- 适用于主线程需要等待多个子线程完成任务的场景。
CountDownLatch latch = new CountDownLatch(3);
Runnable task = () -> {
try {
// 执行任务
} finally {
latch.countDown(); // 任务完成,计数器减一
}
};
new Thread(task).start();
new Thread(task).start();
new Thread(task).start();
latch.await(); // 等待所有任务完成
System.out.println("所有任务都完成了");
14. Synchronized 和 ReentrantLock 有什么区别?
- Synchronized 是 Java 内置的关键字,实现基本的同步机制,非公平,不可中断,不支持多条件,不支持超时。是可重入锁, 不需要手动加锁解锁
- ReentrantLock 是 JUC 类库提供的,由 JDK 1.5 引入,支持公平锁,可中断,支持多条件判断,并且支持设置超时时间,可以避免死锁,比较灵活。同样是可重入锁, 需要手动加锁解锁
15. 单例模式有哪几种实现?如何保证线程安全?
- 饿汉式:实例在类加载时就创建,线程安全,但如果实例初始化较重或没有被使用会浪费资源。
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
- 懒汉式:实例在首次访问时创建,节约资源,但需要确保线程安全。
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
- 双重检查锁定:在懒汉式的基础上优化,直接加锁效率太低,双重检查锁只在第一次检查实例为空时加锁,提高性能。
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
- 静态内部类:利用类加载机制实现懒加载和线程安全,推荐使用。
public class StaticSingleton {
private StaticSingleton() {}
private static class Inner {
private static final StaticSingleton INSTANCE = new StaticSingleton();
}
public static StaticSingleton getInstance() {
return Inner.INSTANCE;
}
}
- 枚举单例(Java 特有):通过枚举实现单例,简单且防止反射和序列化攻击。
public enum EnumSingleton {
INSTANCE;
public void test() {
// 一些业务逻辑方法
}
}
//使用
EnumSingleton singleton = EnumSingleton.INSTANCE;
singleton.test();
16. Java 的 synchronized 是怎么实现的?
synchronized 实现原理依赖于 JVM 的 Monitor(监视器锁) 和 对象头(Object Header)。
-
当 synchronized 修饰在方法或代码块上时,会对特定的对象或类加锁,从而确保同一时刻只有一个线程能执行加锁的代码块。
- synchronized 修饰方法:当 synchronized 修饰方法时,JVM 会在方法的字节码中添加一个 ACC_SYNCHRONIZED 标志。每次线程调用该方法时,JVM 会检查该标志。
- 如果方法被标记为 synchronized,线程必须先获取该方法所属对象的监视器锁(monitor)。
- 如果是实例方法,锁对象是 this;如果是静态方法,锁对象是类的 Class 对象。
- 获取锁后,线程可以执行方法体,方法执行完成后释放锁。
- synchronized 修饰代码块:当 synchronized 修饰代码块时,JVM 在字节码层面插入 monitorenter 和 monitorexit 指令:。
- monitorenter:在进入代码块时,线程尝试获取指定对象的监视器锁。
- 如果锁未被占用,线程获取锁并继续执行。
- 如果锁已被其他线程占用,当前线程进入阻塞状态,等待锁释放。
- monitorexit:在退出代码块时,线程释放监视器锁。
- 如果代码块中发生异常,JVM 会确保在异常传播到外部之前,monitorexit 指令被正确执行,从而释放锁。
- monitorenter:在进入代码块时,线程尝试获取指定对象的监视器锁。
- synchronized 修饰方法:当 synchronized 修饰方法时,JVM 会在方法的字节码中添加一个 ACC_SYNCHRONIZED 标志。每次线程调用该方法时,JVM 会检查该标志。
-
锁升级总结:
- 偏向锁:当一个线程第一次获取锁时,JVM 会将该线程标记为“偏向”状态,后续若该线程再获取该锁,几乎没有开销。
- 轻量级锁:当另一个线程尝试获取已经被偏向的锁时,锁会升级为轻量级锁,使用 CAS 操作来减少锁竞争的开销。
- 重量级锁:当 CAS 失败无法获取锁,锁会升级为重量级锁,线程会被挂起,直到锁被释放。
-
锁消除和锁粗化
- 锁消除:JVM 会通过逃逸分析判断对象是否只在当前线程使用,如果是,那么会消除不必要的加锁操作。
- 锁粗化:当多个锁操作频繁出现时,JVM 会将这些锁操作合并,减少锁获取和释放的开销。
17. 如何优化 Java 中的锁的使用?
- 减小锁的粒度(使用的时间):
- 尽量缩小加锁的范围,减少锁的持有时间。即在必要的最小代码块内使用锁,避免对整个方法或过多代码块加锁。
- 使用更细粒度的锁,比如将一个大对象锁拆分为多个小对象锁,以提高并行度(参考 HashTable 和ConcurrentHashMap 的区别)。
- 对于读多写少的场景,可以使用读写锁(ReentrantReadWriteLock)代替独占锁。
- 减少锁的使用:
- 通过无锁编程、CAS(Compare-And-Swap)操作和原子类(如 AtomicInteger、AtomicReference)来避免使用锁,从而减少锁带来的性能损耗。
- 通过减少共享资源的使用,避免线程对同一个资源的竞争。例如,使用局部变量或线程本地变量(ThreadLocal)来减少多个线程对同一资源的访问。
18. Java 中 volatile 关键字的作用是什么?
volatile 它的主要作用是保证变量的可见性和禁止指令重排优化。
- 可见性(Visibility):
volatile 关键字确保变量的可见性。当一个线程修改了 volatile 变量的值,新值会立即被刷新到主内存中,其他线程在读取该变量时可以立即获得最新的值。这样可以避免线程间由于缓存一致性问题导致的“看见”旧值的现象。 - 禁止指令重排序(Ordering):
volatile 还通过内存屏障来禁止特定情况下的指令重排序,从而保证程序的执行顺序符合预期。对 volatile 变量的写操作会在其前面插入一个 StoreStore 屏障,而对 volatile 变量的读操作则会在其后插入一个 LoadLoad 屏障。这确保了在多线程环境下,某些代码块执行顺序的可预测性。
19. Java线程的生命周期在 Java 中是如何定义的?
-
Java线程的生命周期由其状态决定,这些状态定义了线程在其生命周期中的不同阶段。线程的状态主要由 java.lang.Thread.State 枚举定义,包括以下几种:
- NEW(新建状态)
当线程被创建但尚未启动时,线程处于 NEW 状态。例如,通过 new Thread() 创建线程后,线程处于此状态。 - RUNNABLE(可运行状态)
当线程调用 start() 方法后,线程进入 RUNNABLE 状态。在这个状态下,线程可能正在运行,也可能正在等待 CPU 资源。 - BLOCKED(阻塞状态)
当线程试图获取一个被其他线程持有的锁时,线程进入 BLOCKED 状态。例如,在同步代码块中等待锁释放。 - WAITING(等待状态)
当线程调用 Object.wait()、Thread.join() 或 LockSupport.park() 等方法时,线程进入 WAITING 状态。这些方法需要被其他线程唤醒。 - TIMED_WAITING(计时等待状态)
当线程调用 Object.wait(long)、Thread.sleep(long) 或 LockSupport.parkNanos() 等方法时,线程进入 TIMED_WAITING 状态。这种状态的线程会在指定的时间后自动唤醒。 - TERMINATED(终止状态)
当线程的执行完成或因为异常退出时,线程进入 TERMINATED 状态。一旦线程进入此状态,它将无法再被启动。
- NEW(新建状态)
-
线程状态之间的转换关系如下:
- NEW -> RUNNABLE:调用 start() 方法。
- RUNNABLE -> BLOCKED:试图获取锁。
- RUNNABLE -> WAITING:调用 wait() 或 join()。
- RUNNABLE -> TIMED_WAITING:调用 sleep() 或 wait(long)。
- RUNNABLE -> TERMINATED:执行完成或异常退出。
- BLOCKED -> RUNNABLE:获取锁。
- WAITING -> RUNNABLE:被唤醒。
- TIMED_WAITING -> RUNNABLE:时间到期或被唤醒。
==注意事项==
线程一旦进入 TERMINATED 状态,就无法再被启动,否则会抛出 IllegalThreadStateException。
评论区