一、线程
01、Java中创建线程的三种方式及其优劣对比?
1.继承Thread类
java
//继承Thread类
class MyThread extends Thread {
private String threadName;
public MyThread(String name) {
this.threadName = name;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(threadName + " 运行: " + i);
try {
Thread.sleep(500);//模拟耗时操作
} catch (InterruptedException e) {
System.out.println(threadName + " 被中断");
}
}
System.out.println(threadName + " 结束");
}
}
//使用示例
public class ThreadExample {
public static void main(String[] args) {
MyThread thread1 = new MyThread("线程A");
MyThread thread2 = new MyThread("线程B");
thread1.start();//启动线程
thread2.start();
}
}
优劣分析
- 优点:
- 编码简单直观,适合初学者。
- 直接调用 start() 方法即可启动线程。
- 缺点:
- 由于Java是单继承,继承了Thread类后不能再继承其他类。
- 任务与线程绑定,不符合面向对象的设计原则。
- 不适合资源共享。
2.实现Runnable接口
java
//实现RunnabLe接口
class MyRunnable implements Runnable {
private String threadName;
public MyRunnable(String name) {
this.threadName = name;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(threadName + " 运行: " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
System.out.println(threadName + "被中断");
}
}
System.out.println(threadName + " 结束");
}
}
//使用示例
public class RunnableExample {
public static void main(String[] args) {
//创建Runnable实例
MyRunnable runnable1 = new MyRunnable("线程A");
MyRunnable runnable2 = new MyRunnable("线程B");
//创建Thread对象并传入Runnable
Thread thread1 = new Thread(runnable1);
Thread thread2 = new Thread(runnable2);
thread1.start();
thread2.start();
//也可以使用Lambda表达式(Java8+)
Thread lambdaThread = new Thread(() -> {
for (int i = 0; i < 3; i++) {
System.out.println("Lambda线程运行: " + i);
}
});
lambdaThread.start();
}
}
优劣分析
- 优点:
- 可以继承其他类,扩展性好。
- 适合多个线程共享同一个资源。
- 任务与线程分离,符合面向对象设计。
- 可以使用线程池管理。
- 缺点:
- 编程稍微复杂一些。
3.实现Callable接口
java
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
//实现Callable接口
class Mycallable implements Callable<String> {
private String threadName;
private int taskcount;
public Mycallable(String name, int count) {
this.threadName = name;
this.taskcount = count;
}
@Override
public String call() throws Exception {
int sum = 0;
for (int i = 1; i <= taskcount; i++) {
sum += i;
System.out.println(threadName + " 计算: 1到" + i + "的和=" + sum);
Thread.sleep(300);
}
return threadName + "计算结果: 1到" + taskcount + "的和=" + sum;
}
}
public class CallableExample {
public static void main(String[] args) {
//创建Callable任务
Mycallable callable1 = new Mycallable("任务A", 5);
Mycallable callable2 = new Mycallable("任务B", 3);
//创建FutureTask包装Callable
FutureTask<String> futureTask1 = new FutureTask<>(callable1);
FutureTask<String> futureTask2 = new FutureTask<>(callable2);
//创建线程并启动
Thread thread1 = new Thread(futureTask1);
Thread thread2 = new Thread(futureTask2);
thread1.start();
thread2.start();
//获取执行结果
try {
String result1 = futureTask1.get();//会阻塞直到任务完成
String result2 = futureTask2.get();
System.out.println("任务A结果:" + result1);
System.out.println("任务B结果:" + result2);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
优劣分析
-
优点:
-
可以获取线程执行结果。
-
可以抛出异常,便于错误处理。
-
支持泛型返回值。
-
可以与线程池完美配合。
-
-
缺点:
-
编码最复杂。
-
获取结果时会阻塞当前线程。
-
4.开发建议
推荐使用实现 Runnable 接口或 Callable 接口的方式,因为:
- 更好的设计:符合面向对象的设计原则,任务与执行机制分离。
- 更好的扩展性:可以继承其他类。
- 线程池支持:可以充分利用线程池管理线程。
- 资源共享:多个线程可以共享同一个Runnable实例。
java
import java.util.concurrent.*;
public class RecommendedPractice {
public static void main(String[] args) {
//使用线程池+Runnable/Callable
ExecutorService executor = Executors.newFixedThreadPool(3);
//提交Runnable任务
executor.execute(() -> System.out.println("Runnable任务执行中..."));
//提交Callable任务并获取Future
Future<String> future = executor.submit(() -> {
Thread.sleep(1000);
return "Callable任务结果";
});
try {
//获取Callable任务的结果(可选)
String result = future.get();
System.out.println("Callable任务结果: " + result);
} catch (Exception e) {
e.printStackTrace();
}
//关闭线程池
executor.shutdown();
}
}
02、线程的六大状态(NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED)如何转换?
线程的状态在(java.lang.Thread.State)枚举中明确定义:
- NEW(新建):线程被创建,但尚未启动。即未调用 start() 方法。
- RUNNABLE(可运行):线程正在JVM中执行,或者正在等待操作系统的CPU资源。注意:它包含了操作系统线程状态中的Ready(就绪)和Running(运行中)。
- BLOCKED(阻塞):线程等待获取一个监视器锁(synchronized锁)而进入的状态。这个状态是因为互斥,其他线程持有了该锁。
- WAITING(无限等待):线程进入等待状态,需要等待其他线程做出一些特定动作(通知或中断)。进入此状态需要调用特定方法。
- TIMED_WAITING(超时等待):线程在指定的时间内等待,无需等待其他线程显式地唤醒,到时间后会被系统自动唤醒。
- TERMINATED(终止):线程已经执行完毕(run方法正常结束)或者因异常而退出。
1.NEW->RUNNABLE
- 转换条件:调用线程的 start() 方法。
- 说明:start() 方法会为线程分配必要的系统资源,并安排其执行。注意:调用 run() 方法并不会启动新线程,它只是在当前线程中同步执行方法体。
2.RUNNABLE->TERMINATED
- 转换条件:线程的 run() 方法正常执行完毕,或者执行过程中抛出了一个未被捕获的异常(Exception 或 Error)。
- 说明:一旦进入终止状态,线程的生命周期就结束了,不能再次通过 start() 方法唤醒。
3.RUNNABLE<->BLOCKED
- RUNNABLE->BLOCKED:线程尝试进入一个 synchronized 同步代码块或方法时,但该对象的监视器锁已被其他线程持有。此时,该线程会变为 BLOCKED 状态。
- BLOCKED->RUNNABLE:当持有该对象锁的线程释放锁后,等待队列中的所有 BLOCKED 线程会去争抢锁,抢到锁的线程会重新回到 RUNNABLE 状态。
4.RUNNABLE->WAITING
线程在 RUNNABLE 状态下调用以下方法会进入 WAITING 状态:
- Object.wait():调用前必须持有对象的锁。调用后,会释放锁并进入等待,直到其他线程调用该对象的 notify() 或 notifyAll() 。
- Thread.join():等待目标线程执行终止。例如,在 threadA 中调用 threadB.join(),则 threadA 会等待 threadB 终止。
- LockSupport.park():通过 Unsafe 类提供的许可机制挂起当前线程。
5.WAITING->RUNNABLE
转换条件:取决于进入 WAITING 状态时调用的方法:
- 由 Object.wait() 进入的,需要其他线程调用同一个对象的 Object.notify() 或 Object.notifyAll()。
- 由 Thread.join() 进入的,需要被等待的线程执行完毕(进入TERMINATED)。
- 由 LockSupport.park() 进入的,需要其他线程调用 LockSupport.unpark(目标线程)。
6.RUNNABLE->TIMED_WAITING
线程在 RUNNABLE 状态下调用以下带超时参数的方法会进入 TIMED_WAITING 状态:
- Thread.sleep(long millis) :让当前线程睡眠指定毫秒数,不释放锁。
- Object.wait(long timeout) :带超时的等待,在指定时间内若未被唤醒,会自动醒来。
- Thread.join(long millis) :带超时的Join,最多等待目标线程指定毫秒数。
- LockSupport.parkNanos(long nanos) / parkUntil(long deadline) :带超时的挂起。
7.TIMED_WAITING->RUNNABLE
转换条件:
- 超时时间到期。
- 在超时时间内,被其他线程通过对应的唤醒方法唤醒(如 Object.notify() 唤醒 0bject.wait(long) )。
- 等待的目标线程终止(针对 Thread.join(long) )。
03、守护线程与用户线程的区别?如何设置?
1.核心区别
- 用户线程:普通的、完成主要业务逻辑的线程。JVM会等待所有用户线程执行完毕才会退出。
- 守护线程:在后台提供通用服务的"陪伴"线程。JVM不等待守护线程,所有用户线程结束时,守护线程自动被强制终止。
- 最核心的区别:JVM的退出行为。只要还有一个用户线程在运行,JVM就不会退出。而当所有用户线程都结束时,JVM会立即"杀死"所有仍在运行的守护线程,然后退出。
2.如何设置
通过调用 Thread 对象的 setDaemon(boolean on) 方法进行设置。关键点:
- 必须在 start() 方法调用之前设置,否则会抛出 llegalThreadStateException 。
- 参数为 true 表示设置为守护线程。
java
Thread thread = new Thread(new MyRunnable());
thread.setDaemon(true);//设置为守护线程
thread.start();//启动线程
3.代码示例说明
示例1:用户线程(阻止 JVM 退出)
分析:尽管main主线程已经结束,但JVM检测到还有一个用户线程 userThread 在运行,所以JVM不会退出,而是等待它执行完5次循环并正常结束。
java
public class UserThreadExample {
public static void main(String[] args) {
Thread userThread = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
try {
Thread.sleep(1000);//模拟任务执行
System.out.println("用户线程正在运行..." + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("用户线程执行完毕。");
});
//不设置,默认为用户线程
// userThread.setDaemon(false);
userThread.start();
//主线程(也是一个用户线程)先结束
System.out.println("主线程执行完毕,即将结束。");
}
}
示例2:守护线程(不阻止 JVM 退出)
分析:
- main主线程是唯一的用户线程。
- 主线程执行完毕后,JVM检查发现没有其他存活的用户线程。
- JVM立即强制终止仍在运行的守护线程daemonThread(它才运行了4次,计划是10次)。
- 因此,守护线程中的最后一条打印语句"守护线程执行完毕。"没有机会执行。
java
public class DaemonThreadExample {
public static void main(String[] args) throws InterruptedException {
Thread daemonThread = new Thread(() -> {
for (int i = 1; i <= 10; i++) {// 计划运行10次
try {
Thread.sleep(1000);
System.out.println("守护线程正在运行..." + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//由于JVM退出,这行代码大概率不会被执行
System.out.println("守护线程执行完毕。");
});
//关键步骤:设置为守护线程
daemonThread.setDaemon(true);
daemonThread.start();
//主线程(用户线程)先结束
Thread.sleep(5000);
System.out.println("主线程执行完毕,即将结束。");
}
}
/*
守护线程正在运行...1
守护线程正在运行...2
守护线程正在运行...3
守护线程正在运行...4
主线程执行完毕,即将结束。
*/
04、为什么优先用Runnable而非Thread继承实现线程?
推荐做法:始终优先使用 Runnable 接口,结合线程池来管理线程生命周期,这样代码更加灵活、可维护且性能更好。
1.避免单继承限制
Java是单继承语言,如果继承了Thread类,就无法再继承其他类。
java
//使用Thread继承的方式 - 限制继承
class MyThread extends Thread {
//现在这个类不能再继承其他类了!
public void run() {
System.out.println("Thread running");
}
}
//使用Runnable接口的方式 - 更灵活
class MyTask implements Runnable {
//这个类还可以继承其他类
public void run() {
System.out.println("Task running");
}
}
class BusinessClass {
}
class MyBusinessTask extends BusinessClass implements Runnable {
public void run() {
//可以同时继承业务类和实现线程功能
System.out.println("Business task running");
}
}
2.代码职责更清晰
Runnable明确表示这是一个可执行的任务,而不是一个线程实体。
java
//Thread继承方式-职责混淆
class LoggerThread extends Thread {
private String message;
public LoggerThread(String message) {
this.message = message;
}
public void run() {
System.out.println("Log: " + message);
}
}
//Runnable方式-职责清晰
class LoggingTask implements Runnable {
private String message;
public LoggingTask(String message) {
this.message = message;
}
public void run() {
System.out.println("Log: " + message);
}
}
public class ResponsibilityDemo {
public static void main(String[] args) {
//Thread方式-看起来像创建了一个线程,但实际上只是执行一个简单任务
Thread loggerl = new LoggerThread("Hello");
loggerl.start();
//Runnable方式-明确表示这是一个任务
Runnable task = new LoggingTask("Hello");
Thread worker = new Thread(task);
worker.start();
}
}
3.资源共享和线程池友好
Runnable 实例可以被多个线程共享,更适合线程池。
java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class CounterTask implements Runnable {
private int count = 0;
public void run() {
for (int i = 0; i < 5; i++) {
synchronized (this) {
count++;
System.out.println(Thread.currentThread().getName() + "- Count:" + count);
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class ResourceSharingDemo {
public static void main(String[] args) {
//多个线程共享同一个Runnable实例
CounterTask sharedTask = new CounterTask();
Thread t1 = new Thread(sharedTask, "Thread-1");
Thread t2 = new Thread(sharedTask, "Thread-2");
Thread t3 = new Thread(sharedTask, "Thread-3");
t1.start();
t2.start();
t3.start();
//线程池中使用-更加高效
ExecutorService executor = Executors.newFixedThreadPool(3);
Runnable task = new CounterTask();
for (int i = 0; i < 5; i++) {
executor.execute(task);
}
executor.shutdown();
}
}
4.更灵活的任务组合
可以使用 Lambda 表达式和函数式编程。
java
public class FlexibilityDemo {
public static void main(String[] args) {
//使用Lambda表达式创建Runnable
Runnable task1 = () -> {
for (int i = 0; i < 3; i++) {
System.out.println("Lambda task:" + i);
}
};
//使用方法引用
Runnable task2 = FlexibilityDemo::printMessage;
//组合多个任务
Runnable combinedTask = () -> {
task1.run();
task2.run();
};
Thread t1 = new Thread(task1);
Thread t2 = new Thread(combinedTask);
t1.start();
t2.start();
}
private static void printMessage() {
System.out.println("Method reference task");
}
}
5.更好的面向对象设计
java
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class DownloadThread extends Thread {
private String url;
public DownloadThread(String url) {
this.url = url;
}
public void run() {
//下载逻辑
System.out.println("Downloading from: " + url);
}
}
//好的设计-任务与执行分离
class DownloadTask implements Runnable {
private String url;
public DownloadTask(String url) {
this.url = url;
}
public void run() {
//下载逻辑
System.out.println("Downloading from: " + url);
}
}
class FileProcessor implements Runnable {
private String filePath;
public FileProcessor(String filePath) {
this.filePath = filePath;
}
public void run() {
//文件处理逻辑
System.out.println("Processing file: " + filePath);
}
}
public class BetterDesignDemo {
public static void main(String[] args) {
//可以灵活地创建不同类型的任务
List<Runnable> tasks = Arrays.asList(
new DownloadTask("http://example.com/file1"),
new DownloadTask("http://example.com/file2"),
new FileProcessor("/path/to/filel"),
new FileProcessor("/path/to/file2")
);
//统一用线程池执行
ExecutorService executor = Executors.newFixedThreadPool(4);
for (Runnable task : tasks) {
executor.execute(task);
}
executor.shutdown();
}
}
6.性能考虑
java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class PerformanceDemo {
public static void main(String[] args) {
int taskCount = 1000;
//使用Runnable+线程池-资源利用率高
ExecutorService executor = Executors.newFixedThreadPool(10);
Runnable task = () -> {
//模拟任务执行
try {
System.out.println("executor = " + executor);
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
};
long startTime = System.currentTimeMillis();
for (int i = 0; i < taskCount; i++) {
executor.execute(task);
}
executor.shutdown();
try {
executor.awaitTermination(1, TimeUnit.MINUTES);
} catch (InterruptedException e) {
e.printStackTrace();
}
long endTime = System.currentTimeMillis();
System.out.println("Runnable + ThreadPool 执行时间:" + (endTime - startTime) + "ms");
}
}
05、Thread.join() 的作用和底层实现原理?
1.Thread.join() 的作用
核心作用:让当前线程等待目标线程执行结束后再继续执行。
代码示例说明:
java
public class JoinExample {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
try {
Thread.sleep(1000);
System.out.println("线程1执行完成");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread thread2 = new Thread(() -> {
try {
Thread.sleep(500);
System.out.println("线程2执行完成");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread1.start();
thread2.start();
System.out.println("主线程等待线程1完成...");
thread1.join();//主线程在此等待thread1执行完成
System.out.println("线程1已完成,主线程继续执行");
thread2.join();// 等待thread2完成
System.out.println("所有线程执行完毕");
}
}
2.底层实现原理
- 核心源码分析
java
public final void join() throws InterruptedException {
join(0);
}
public final synchronized void join(final long millis) throws InterruptedException {
if (millis > 0) {
if (isAlive()) {
final long startTime = System.nanoTime();
long delay = millis;
do {
wait(delay);
} while (isAlive() && (delay = millis -
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)) > 0);
}
} else if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
throw new IllegalArgumentException("timeout value is negative");
}
}
-
实现原理要点
-
同步机制:
- join() 方法被声明为 synchronized,使用线程对象作为锁。
- 确保在同一时刻只有一个线程能对目标线程执行 join 操作。
-
等待机制:
- 通过 while(isAlive()) 循环检查目标线程是否存活。
- 调用 wait(0) 使当前线程进入等待状态。
- 当目标线程结束时,JVM会自动调用 notifyAll() 唤醒所有等待的线程。
-
线程终止通知:
-
在线程结束时,JVM底层会调用 lock.notifyAll(thread) 。
-
这解释了为什么不需要手动调用 notifyAll() 。
-
-
3.带超时的 join() 方法
java
public class JoinTimeoutExample {
public static void main(String[] args) throws InterruptedException {
Thread longRunningThread = new Thread(() -> {
try {
Thread.sleep(5000);// 执行5秒
System.out.println("长时间任务完成");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
longRunningThread.start();
System.out.println("主线程等待,最多3秒...");
longRunningThread.join(3000);// 最多等待3秒
if (longRunningThread.isAlive()) {
System.out.println("等待超时,主线程继续执行");
} else {
System.out.println("线程在超时前完成");
}
}
}
06、Java如何保证线程关闭的安全性?
核心思想:协作式中断
Java通过 interrupt() 机制向线程发出一个"中断请求",但并不会强制停止它。被中断的线程需要检查中断状态,并决定如何优雅地响应这个请求。
方法一:使用 interrupt() 和 islnterrupted()
这是最标准和推荐的做法。线程在运行循环中定期检查自己的中断状态,并在捕获到 InterruptedException 时正确处理。
安全性保证:
- 控制权在任务线程:任务线程自己决定何时检查中断并退出,保证了它在处理完一个逻辑单元或到达一个安全点后才停止。
- 资源清理:在退出循环后,有机会执行必要的清理工作(如关闭文件流、释放数据库连接等)。
- 状态一致性:不会因为被强制停止而留下中间状态的数据。
java
public class SafeShutdownWithInterrupt implements Runnable {
@Override
public void run() {
//检查中断状态。如果未被中断,则继续执行任务。
while (!Thread.currentThread().isInterrupted()) {
try {
//1.模拟执行工作任务
System.out.println("working...");
//2.在可中断的阻塞操作中(如sleep、wait、IO),会抛出InterruptedException
Thread.sleep(1000);
} catch (InterruptedException e) {
//非常重要!当在sleep时被中断,JVM会清除中断状态并抛出此异常。
//收到异常后,不能假装什么都没发生,有两种做法:
//做法一:恢复中断状态,让循环条件能检测到,从而退出循环。
Thread.currentThread().interrupt();
//做法二:直接退出。
System.out.println("Thread was interrupted during sleep, exiting.");
//break;// 如果选择做法二,就在这里break
}
//执行资源清理工作
System.out.println("Thread is shutting down safely. Cleaning up...");
}
}
public static void main(String[] args) throws InterruptedException {
Thread workerThread = new Thread(new SafeShutdownWithInterrupt());
workerThread.start();
//主线程睡眠3秒,模拟让工作线程运行一段时间
Thread.sleep(3000);
//发起中断请求,安全地关闭工作线程
System.out.println("Main thread requesting interrupt.");
workerThread.interrupt();
//等待工作线程完全结束
workerThread.join();
System.out.println("Main thread: Worker thread has shut down.");
}
}
方法二:使用自定义的 volatile 标志位
对于一些不涉及可中断阻塞操作的线程,可以使用一个 volatile 布尔变量作为开关。
volatile关键字的作用:
确保一个线程对shutdownRequested的修改能立即被其他线程看到。如果没有volatile,工作线程可能永远看不到主线程修改的值,导致关闭请求失效。
java
public class SafeShutdownWithFlag implements Runnable {
//使用volatile保证可见性,一个线程修改了它,其他线程能立刻看到。
private volatile boolean shutdownRequested = false;
@Override
public void run() {
while (!shutdownRequested) {
//执行工作任务,这里没有可中断的阻塞操作
System.out.println("Working...");
//如果这里有长时间且不可中断的I0操作,这种方法可能无法及时响应关闭请求。
}
//清理资源
System.out.println("Thread is shutting down safely. Cleaning up...");
}
public void shutdown() {
shutdownRequested = true;
}
public static void main(String[] args) throws InterruptedException {
SafeShutdownWithFlag task = new SafeShutdownWithFlag();
Thread workerThread = new Thread(task);
workerThread.start();
Thread.sleep(3000);
//通过调用shutdown方法来请求关闭
System.out.println("Main thread requesting shutdown.");
task.shutdown();
workerThread.join();
System.out.println("Main thread: Worker thread has shut down.");
}
}
方法三:结合ExecutorService(生产环境最佳实践)
在实际开发中,我们通常使用线程池来管理线程。ExecutorService提供了更优雅的关闭方式。
ExecutorService 关闭方法:
- shutdown():温和关闭。不再接受新任务,但会执行完已提交的任务。
- awaitTermination(long timeout, TimeUnit unit):阻塞等待所有任务完成,或者超时,或者当前线程被中断。
- shutdownNow():尝试立即停止所有正在执行的任务(通过调用Thread.interrupt()),并返回等待执行的任务列表。它不保证能停止正在处理的任务,任务线程依然需要响应中断。
java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class SafeShutdownWithExecutor {
public static void main(String[] args) {
//1.创建线程池
ExecutorService executor = Executors.newFixedThreadPool(2);
//2.提交任务
executor.submit(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
System.out.println("Task 1 running...");
Thread.sleep(1000);
} catch (InterruptedException e) {
//收到中断异常,退出循环
System.out.println("Task 1 received interrupt, exiting.");
Thread.currentThread().interrupt();// 恢复状态
}
}
});
//3.优雅关闭线程池
System.out.println("shutting down executor...");
executor.shutdown();//停止接收新任务,并开始关闭现有任务
try {
//等待所有任务终止,设置超时时间
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
System.err.println("Some tasks did not finish in time, forcing shutdown...");
//如果超时后还有任务没结束,强制关闭(不保证安全性)
executor.shutdownNow();
}
} catch (InterruptedException e) {
//如果当前线程在awaitTermination期间被中断,也强制关闭
executor.shutdownNow();
Thread.currentThread().interrupt();
System.out.println("Executor has been shut down.");
}
}
}
总结:如何保证线程关闭的安全性?
- 摒弃强制停止:绝不使用已废弃的stop(),suspend(),resume()方法。
- 采用协作机制:使用 interrupt() 向线程发出信号,让线程自己决定何时以及如何退出。
- 正确处理中断:
- 在循环条件中检查 !Thread.currentThread().isInterrupted()。
- 在捕获 InterruptedException 后,要么恢复中断状态(Thread.currentThread().interrupt()),要么直接退出。不能忽略异常!
- 保证可见性:如果使用自定义标志位,务必将其声明为 volatile 。
- 善用线程池:在生产环境中,优先使用 ExecutorService 及其提供的 shutdown() 和 awaitTermination 方法,这是最规范和安全的管理方式。
通过以上方法,可以确保线程在接收到关闭指令后,能运行到一个"安全"的位置再结束,从而保证数据状态的一致性和资源的安全释放。
07、线程未捕获异常的全局处理机制是什么?
Java线程未捕获异常的全局处理机制
在Java中,当线程执行过程中抛出未捕获的异常时,JVM会调用该线程的 UncaughtExceptionHandler 来处理异常。如果没有设置特定的处理器,则会使用默认的全局处理器。
核心机制
- Thread.UncaughtExceptionHandler 接口
- Thread.setDefaultUncaughtExceptionHandler() 方法设置全局处理器
- Thread.setUncaughtExceptionHandler()方法设置线程特定处理器
代码示例
java
public class UncaughtExceptionHandlerDemo {
public static void main(String[] args) {
//1.设置全局默认的未捕获异常处理器
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("【全局处理器】线程: " + t.getName() + " 发生异常: " + e.getMessage());
System.out.println("【全局处理器】异常类型: " + e.getClass().getSimpleName());
//这里可以记录日志、发送报警等
}
});
//2.创建并启动会抛出异常的线程
Thread normalThread = new Thread(() -> {
System.out.println("普通线程开始执行...");
//模拟运行时异常
throw new RuntimeException("这是一个测试异常");
}, "普通工作线程");
//3.为特定线程设置专属的异常处理器
Thread specialThread = new Thread(() -> {
System.out.println("特殊线程开始执行...");
int result = 10 / 0;//模拟算术异常
}, "特殊工作线程");
//为特殊线程设置专属处理器
specialThread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("【专属处理器】线程: " + t.getName() + " 发生严重错误: " + e.getMessage());
System.out.println("【专属处理器】进行特殊恢复操作...");
}
});
//启动线程
normalThread.start();
specialThread.start();
//主线程继续执行
System.out.println("主线程继续执行...");
//等待子线程结束
try {
normalThread.join();
specialThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("程序执行完毕");
}
}
更完整的实际应用示例
java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ComprehensiveExceptionHandler {
//自定义全局异常处理器
static class GlobalExceptionHandler implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
//在实际应用中,这里可以:
//1.记录错误日志
//2.发送报警通知
//3.统计错误 metrics
//4:执行清理操作
System.err.println("=== 全局异常处理===");
System.err.println("线程名称: " + t.getName());
System.err.println("异常类型: " + e.getClass().getName());
System.err.println("异常信息: " + e.getMessage());
System.err.println("堆栈跟踪: ");
e.printStackTrace();
System.err.println("=== 处理完成 ===");
//注意:这里不能阻止线程终止
}
}
//业务特定的异常处理器
static class BusinessExceptionHandler implements Thread.UncaughtExceptionHandler {
private String businessType;
public BusinessExceptionHandler(String businessType) {
this.businessType = businessType;
}
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("【业务处理器】业务类型: " + businessType);
System.out.println("【业务处理器】线程: " + t.getName() + "执行失败");
if (e instanceof NullPointerException) {
System.out.println("【业务处理器】检测到空指针异常,进行数据回滚");
} else if (e instanceof ArithmeticException) {
System.out.println("【业务处理器】检测到计算异常,使用默认值继续");
}
}
}
public static void main(String[] args) {
//设置全局处理器
Thread.setDefaultUncaughtExceptionHandler(new GlobalExceptionHandler());
//使用线程池测试
ExecutorService executor = Executors.newFixedThreadPool(3);
//提交多个会抛出异常的任务
for (int i = 0; i < 5; i++) {
final int taskId = i;
executor.execute(() -> {
System.out.println("任务 " + taskId + " 开始执行");
//为特定任务设置专属处理器
if (taskId == 2) {
Thread.currentThread().setUncaughtExceptionHandler(new BusinessExceptionHandler("订单处理业务"));
}
//模拟不同的异常情况
switch (taskId % 5) {
case 0:
throw new RuntimeException("任务" + taskId + "的运行时异常");
case 1:
String str = null;
str.length();//空指针异常
break;
case 2:
int result = 10 / 0;// 算术异常
break;
}
System.out.println("任务" + taskId + "正常完成");
});
}
//关闭线程池
executor.shutdown();
}
}
关键要点
- 优先级顺序:
- 线程专属处理器>全局默认处理器
- 如果线程设置了专属处理器,就不会调用全局处理器
- 处理时机:
- 在异常导致线程终止前调用
- 处理器执行完毕后,线程就会终止
- 实际应用场景:
- 日志记录和监控
- 错误统计和报警
- 资源清理
- 优雅降级
- 注意事项:
- 处理器中不应抛出新的异常
- 处理器不能阻止线程终止
08、ThreadLocal的原理、内存泄漏风险及解决方案?
1.ThreadLocal 的核心原理
ThreadLocal 的核心思想是数据隔离,它为每个线程提供一个独立的变量副本,避免了多线程环境下的共享冲突。
底层实现机制
- 线程专属存储:每个 Thread 线程对象内部都有一个 threadLocals 字段,它是 ThreadLocal.ThreadLocalMap 类型的。这个Map就是存储线程局部变量的核心结构。
- 键值对设计:
- Key键:是 ThreadLocal 对象本身的弱引用(WeakReference)。例如,当你声明 ThreadLocal userContext = new ThreadLocal<>(); 时, Map中的Key就是指向 userContext 的弱引用。
- Value值:是通过 threadLocal.set(value) 设置的变量副本,由强引用直接关联。
- 哈希冲突解决:ThreadLocalMap 使用开放地址法(线性探测)来解决哈希冲突,这与 HashMap 的链地址法不同。为了更直观地理解这些引用关系,我们看如下流程图,它清晰地展示了 ThreadLocal 使用中的引用链和内存泄漏的根源。
java
public class ThreadLocalDemo {
//通常声明为static,避免每次创建新的ThreadLocal实例
private static final ThreadLocal<String> userContext = new ThreadLocal<>();
public static void main(String[] args) {
//在主线程设置值
userContext.set("Main_Thread_User");
Thread workerThread = new Thread(() -> {
//在子线程中设置值,独立于主线程
userContext.set("Worker_Thread_User");
System.out.println("Worker thread: " + userContext.get()); //输出 Worker_Thread_User
//使用完毕后清理,防止内存泄漏
userContext.remove();
});
workerThread.start();
System.out.println("Main thread: " + userContext.get());// 输出 Main_Thread_User
userContext.remove();//主线程也清理
}
}
关键点解读
- 数据隔离:主线程和 workerThread 通过同一个 userContext 对象获取到的值是不同的,互不影响。
- 重要提醒:代码中在最后调用了 remove() 方法,这是一个至关重要的好习惯,用于避免内存泄漏。
2.内存泄漏风险详解
根源探究
如上面的流程图所示,ThreadLocalMap中Entry的Key是弱引用指向ThreadLocal对象,而Value是强引用指向存储的对象。
弱引用Key的GC回收:当ThreadLocal对象在外界没有强引用时(例如,设置为null),由于Map中的Key是弱引用,在下次GC时,这个Key就会被回收,Entry的Key变为null。
强引用Value无法回收:尽管Key被回收了,但Entry对象本身仍然存在于Map中,并且其Value还通过强引用链与Thread对象相连(Thread Ref->Thread->ThreadLocalMap->Entry->value)。只要线程还在运行(常见于线程池场景),这个Value就永远不会被GC回收,但我们已经无法再访问到它(因为Key是null),这就造成了内存泄漏。
3.解决方案与最佳实践
-
强制清理:使用 remove 方法
最基本也是最重要的原则:每次使用完ThreadLocal后,必须调用remove()方法清理。
java
public class UserContextHolder {
private static final ThreadLocal<User> context = new ThreadLocal<>();
public static void setUser(User user) {
context.set(user);
}
public static User getUser() {
return context.get();
}
public static void clear() {
context.remove();//清理当前线程的ThreadLocal值
}
}
//在Filter或Interceptor中使用
public class AuthFilter {
void doFilter() {
User user = loadUser();
UserContextHolder.setUser(user);
try {
//业务处理
processBusiness();
} finally {
UserContextHolder.clear();//确保最终清理
}
}
}
-
使用 static 修饰 ThreadLocal
将 ThreadLocal 变量声明为 static ,这主要是为了避免创建多个 ThreadLocal 实例。如果为每个线程都创建一个新的 ThreadLocal 实例,那么每个线程的 ThreadLocalMap 中会存储多个 Entry ,即使你清除了其中一个,其他的 Entry 可能仍然会造成泄漏。
在面试中,清晰地阐述 ThreadLocal 的原理,并强调 remove() 方法的重要性,通常能体现出你对内存管理的深刻理解。
09、如何利用ThreadGroup管理线程组?
-
创建线程组和线程
javaclass WorkerThread extends Thread { public WorkerThread(ThreadGroup group, String name) { super(group, name); } @Override public void run() { try { for (int i = 0; i < 5; i++) { System.out.println(getName() + "执行任务" + i); Thread.sleep(1000); } } catch (InterruptedException e) { System.out.println(getName() + "被中断"); } } } public class ThreadGroupDemo { public static void main(String[] args) { //创建线程组 ThreadGroup group = new ThreadGroup("WorkerGroup"); //创建并启动属于该线程组的多个线程 for (int i = 1; i <= 3; i++) { WorkerThread worker = new WorkerThread(group, "worker-" + i); worker.start(); } //监控线程组状态 monitorThreadGroup(group); } static void monitorThreadGroup(ThreadGroup group) { Thread monitor = new Thread(() -> { while (group.activeCount() > 0) { StringBuffer buffer = new StringBuffer(); buffer.append("活跃线程数: " + group.activeCount()); buffer.append("\n线程组名称: " + group.getName()); buffer.append("\n父线程组: " + (group.getParent() != null ? group.getParent().getName() : "无")); //列出所有活跃线程 Thread[] threads = new Thread[group.activeCount()]; group.enumerate(threads); for (Thread t : threads) { if (t != null) { buffer.append("\n - " + t.getName() + ": " + t.getState()); } } try { System.err.println(buffer); Thread.sleep(2000); } catch (InterruptedException e) { break; } } }); monitor.start(); } } -
批量管理线程
javaclass TaskThread extends Thread { public TaskThread(ThreadGroup group, String name) { super(group, name); } @Override public void run() { try { while (!isInterrupted()) { System.out.println(getName() + " 正在工作..."); Thread.sleep(1000); } } catch (InterruptedException e) { System.out.println(getName() + "接收到中断信号"); } } } public class ThreadGroupManagement { public static void main(String[] args) throws InterruptedException { ThreadGroup taskGroup = new ThreadGroup("TaskGroup"); //创建多个工作线程 for (int i = 1; i <= 5; i++) { new TaskThread(taskGroup, "TaskThread-" + i).start(); } Thread.sleep(3000); //批量设置线程组属性 taskGroup.setMaxPriority(Thread.NORM_PRIORITY); System.out.println("线程组最大优先级: " + taskGroup.getMaxPriority()); //中断线程组中的所有线程 System.out.println("中断所有线程..."); taskGroup.interrupt(); //等待所有线程结束 while (taskGroup.activeCount() > 0) { Thread.sleep(500); } System.out.println("所有线程已结束"); } } -
线程组层次结构
javaclass Worker extends Thread { public Worker(ThreadGroup group, String name) { super(group, name); } @Override public void run() { try { for (int i = 0; i < 3; i++) { System.out.println(getName() + "执行第" + i + "次任务"); Thread.sleep(1500); } } catch (InterruptedException e) { System.out.println(getName() + " 被中断"); } } } public class ThreadGroupHierarchy { public static void main(String[] args) { //创建层次化的线程组结构 ThreadGroup parentGroup = new ThreadGroup("ParentGroup"); ThreadGroup childGroup1 = new ThreadGroup(parentGroup, "ChildGroup1"); ThreadGroup childGroup2 = new ThreadGroup(parentGroup, "ChildGroup2"); //在各个线程组中创建线程 new Worker(parentGroup, "ParentWorker").start(); new Worker(childGroup1, "Child1worker").start(); new Worker(childGroup2, "Child2worker").start(); //显示线程组层次结构 displayGroupHierarchy(parentGroup, 0); //递归中断所有线程 interruptGroupAndSubgroups(parentGroup); } //递归显示线程组层次结构 static void displayGroupHierarchy(ThreadGroup group, int level) { StringBuilder indent = new StringBuilder(); for (int i = 0; i < level; i++) { indent.append(" "); } System.err.println(indent + "L " + group.getName() + "(活跃线程: " + group.activeCount() + ")"); //获取子线程组 ThreadGroup[] subGroups = new ThreadGroup[group.activeGroupCount()]; group.enumerate(subGroups, false); for (ThreadGroup subGroup : subGroups) { if (subGroup != null) { displayGroupHierarchy(subGroup, level + 1); } } } //递归中断线程组及其所有子组中的线程 static void interruptGroupAndSubgroups(ThreadGroup group) { group.interrupt(); ThreadGroup[] subGroups = new ThreadGroup[group.activeGroupCount()]; group.enumerate(subGroups, false); for (ThreadGroup subGroup : subGroups) { if (subGroup != null) { interruptGroupAndSubgroups(subGroup); } } } } -
线程组异常处理
javaimport java.util.concurrent.atomic.AtomicInteger; class FaultyThread extends Thread { private static final AtomicInteger counter = new AtomicInteger(0); public FaultyThread(ThreadGroup group, String name) { super(group, name); } @Override public void run() { try { //模拟工作 Thread.sleep(1000); //每3次执行抛出一次异常 if (counter.incrementAndGet() % 3 == 0) { throw new RuntimeException("模拟的业务异常"); } System.out.println(getName() + "正常完成工作"); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } public class ThreadGroupExceptionHandler { public static void main(String[] args) { //创建带异常处理的线程组 ThreadGroup safeGroup = new ThreadGroup("SafeGroup") { @Override public void uncaughtException(Thread t, Throwable e) { System.out.println("线程 " + t.getName() + " 发生异常: " + e.getMessage()); System.out.println("重启线程..."); //可以在这里实现重启逻辑 new FaultyThread(this, t.getName() + "-Restarted").start(); } }; //创建可能抛出异常的线程 for (int i = 1; i <= 3; i++) { new FaultyThread(safeGroup, "FaultyThread-" + i).start(); } } }
10、如何用Condition实现精准的线程唤醒?
在Java中,使用Condition可以实现精准的线程唤醒,解决传统 wait/notify 无法指定唤醒线程类型的问题。
关键要点说明:
1.精准唤醒机制:
- notEmpty:消费者等待的条件,当队列为空时阻塞。
- notFull:生产者等待的条件,当队列满时阻塞。
- 生产者通过 notEmpty.signal() 只唤醒消费者。
- 消费者通过 notFull.signal() 只唤醒生产者。
2.避免虚假唤醒:使用while循环而非if判断,防止虚假唤醒导致的条件判断失效。
java
while (queue.size() == capacity) {
System.out.println(Thread.currentThread().getName() + ": 队列已满,生产者等待");
notFull.await();//生产者等待"非满"条件
}
3.锁的使用规范:
- 必须先在 lock() 中获取锁才能调用 Condition 方法。
- 在finally块中释放锁确保异常时也能正常解锁。
4.执行流程示例:
Consumer-1: 队列为空,消费者等待
Producer-1: 生产 1,队列大小: 1
Consumer-1: 消费 1,队列大小: 0
Producer-1: 生产 2,队列大小: 1
Consumer-1: 消费 2,队列大小: 0
Producer-1: 生产 3,队列大小: 1
Consumer-1: 消费 3,队列大小: 0
Producer-1: 生产 4,队列大小: 1
Producer-1: 生产 5,队列大小: 2
Consumer-1: 消费 4,队列大小: 1
Producer-1: 生产 6,队列大小: 2
Consumer-1: 消费 5,队列大小: 1
Producer-1: 生产 7,队列大小: 2
Producer-1: 生产 8,队列大小: 3
Consumer-1: 消费 6,队列大小: 2
Producer-1: 生产 9,队列大小: 3
Consumer-1: 消费 7,队列大小: 2
Producer-1: 生产 10,队列大小: 3
Consumer-1: 消费 8,队列大小: 2
Consumer-1: 消费 9,队列大小: 1
Consumer-1: 消费 10,队列大小: 0
5.与传统 wait/notify 的对比优势:
- 传统方式:使用 notify() 时随机唤醒一个线程,可能唤醒的是同类型线程。
- Condition方式:可以精确控制唤醒的线程类型,提高系统效率。
下面通过生产者-消费者示例演示:
java
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ProducerConsumerWithCondition {
private final Queue<Integer> queue = new LinkedList<>();
private final int capacity = 5;
private final Lock lock = new ReentrantLock();
//创建两个Condition实现精准唤醒
private final Condition notEmpty = lock.newCondition();//消费者等待条件
private final Condition notFull = lock.newCondition();//生产者等待条件
//生产者方法
public void produce(int value) throws InterruptedException {
lock.lock();
try {
//使用while循环防止虚假唤醒
while (queue.size() == capacity) {
System.out.println(Thread.currentThread().getName() + ": 队列已满,生产者等待");
notFull.await();//生产者等待"非满"条件
}
queue.offer(value);
System.out.println(Thread.currentThread().getName() + ": 生产 " + value + ",队列大小: " + queue.size());
notEmpty.signal();//精准唤醒消费者线程
} finally {
lock.unlock();
}
}
//消费者方法
public void consume() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
System.out.println(Thread.currentThread().getName() + ": 队列为空,消费者等待");
notEmpty.await();//消费者等待"非空"条件
}
int value = queue.poll();
System.out.println(Thread.currentThread().getName() + ": 消费 " + value + ",队列大小: " + queue.size());
notFull.signal();//精准唤醒生产者线程
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ProducerConsumerWithCondition pc = new ProducerConsumerWithCondition();
//创建生产者线程
Thread producerl = new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
pc.produce(i);
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}, "Producer-1");
//创建消费者线程
Thread consumerl = new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
pc.consume();
Thread.sleep(150);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}, "Consumer-1");
producerl.start();
consumerl.start();
}
}
二、锁
01、synchronized的字节码层面实现?
synchronized 关键字在字节码中是通过 monitorenter 和 monitorexit 这一对指令来实现的,同时配合方法的 ACC_SYNCHRONIZED 访问标志。
场景一:同步代码块
当 synchronized 被用于修饰一个代码块时,编译后会在代码块开始的位置插入 monitorenter 指令,在代码块结束和异常处插 monitorexit 指令。
字节码层面实现解析:
-
monitorenter :
- 指令执行前,操作数栈顶必须是要锁定的对象引用(这里是lock)。
- JVM会尝试获取该对象的监视器(Monitor):
- 如果该监视器未被任何线程持有,当前线程会成功获取锁,并将锁的计数器置为1。
- 如果当前线程已经持有该监视器,那么它可以重入此锁,锁计数器加1。
- 如果该监视器被其他线程持有,当前线程会被阻塞,直到锁被释放。
-
monitorexit :
-
指令执行前,操作数栈顶同样是要解锁的对象引用。
-
该指令会将关联的监视器的计数器减1。
-
当计数器减到0时,锁被完全释放,其他被阻塞的线程可以尝试获取它。
-
注意:字节码中包含了两个monitorexit指令。第一个用于同步块正常退出,第二个用于处理同步块内抛出异常的情况。这确保了无论代码块以何种方式退出(正常或异常),锁都能被正确释放,避免了死锁。这是编译器自动为我们实现的保障。
-
java
public class synchronizedDemo {
private final Object lock = new Object();
private int count = 0;
public void increment() {
//同步代码块
synchronized (lock) {
count++;
}
}
}
场景二:同步方法
当 synchronized 用于修饰一个方法时,它不会在方法体内生成 monitorenter 和 monitorexit 指令,而是通过方法的访问标志ACC_SYNCHRONIZED 来标识。
字节码层面实现解析:
-
ACC_SYNCHRONIZED
-
这是一个方法级的同步标识。当JVM的方法调用指令(如invokevirtual)检查到这个标志时,它会在调用方法之前,自动去获取对应对象的监视器。
-
对于实例同步方法,锁对象是this(当前实例对象)。
-
对于静态同步方法,锁对象是该类的Class对象(如SynchronizedDemo.class)。
-
-
如果当前线程成功获取到锁,则执行方法体。
-
当方法执行完成(无论是正常返回还是抛出异常)后,JVM会自动释放该监视器。
-
这个过程的本质与同步代码块是相同的,只是实现方式更加隐式和简洁。
两者的底层实现都依赖于JVM为每个对象关联的一个监视器(Monitor),这个监视器管理着线程的互斥与协作。
java
public class synchronizedDemo {
private int count = 0;
public synchronized void increment() {
count++;
}
}
02、synchronized锁升级过程?
锁升级的核心目的
锁升级是JVM为了在线程竞争程度不同的场景下,平衡性能开销和执行效率而设计的优化手段。其核心思想是:在无竞争或低竞争时,使用开销极小的锁;只有当竞争确实激烈时,才使用开销大但能避免CPU空转的重量级锁。
锁升级过程详解
整个升级过程是不可逆的,随着竞争加剧,锁只会从低级别升级到高级别。
-
无锁(No Locking)
-
对象初始状态:一个新创建的对象,还没有任何线程访问它时,处于无锁状态。
-
特点:没有锁,所有线程都可以自由访问。
-
-
偏向锁(Biased Locking)
-
触发条件:第一个线程来请求锁时。
-
核心思想:在接下来的过程中,如果始终只有一个线程在请求这把锁,那么JVM会认为这个线程"偏心了",从而消除这个线程后续的同步开销。这个线程再次进入同步块时,甚至不需要执行CAS操作。
-
实现:在对象头的MarkWord中记录当前持有锁的线程ID。
-
优点:对于同一个线程重复获取锁的情况,开销几乎为零。
-
升级:一旦有第二个线程尝试获取这个锁,偏向锁就会立即撤销,并升级为轻量级锁。
-
-
轻量级锁(Lightweight Locking)
- 触发条件:当有线程竞争偏向锁,导致偏向锁被撤销。
- 核心思想:假设竞争是短暂且轻微的。线程不会直接阻塞,而是通过自旋(CAS操作)来尝试获取锁。
- 实现:每个线程会在自己的栈帧中创建一个名为锁记录(LockRecord)的空间,然后通过CAS操作尝试将对象头的MarkWord更新为指向自己锁记录的指针。如果更新成功,则获取锁。
- 优点:在竞争不激烈的情况下,避免了线程从用户态切换到内核态的阻塞开销。
- 升级:如果线程自旋了一定次数(或自旋了一段时间)后还没有获取到锁,或者此时有第三个及以上的线程来竞争,锁就会升级为重量级锁。
-
重量级锁(Heavyweight Locking)
- 触发条件:轻量级锁竞争失败(自旋失败)
- 核心思想:将竞争处理的权力交给操作系统内核。未能获取锁的线程会被挂起(Park),放入一个等待队列中,等待锁释放后被唤醒。
- 实现:依赖于操作系统底层的互斥量(Mutex)。
- 优点:在激烈竞争下,避免了CPU因线程空转而造成的资源浪费。
- 缺点:线程挂起和唤醒会带来巨大的开销,涉及用户态到内核态的切换。
案例代码与过程分析
下面我们通过一段代码来模拟并解释这个过程。注意:由于JVM的启动优化,为了清晰地看到每个阶段,我们需要在JVM参数中关闭偏向锁延迟(-XX:BiasedLockingStartupDelay=0),并确保对象已计算过HashCode。
xml
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.17</version> <!-- 建议使用较新的版本 -->
</dependency>
java
import org.openjdk.jol.info.ClassLayout;
public class SynchronizedUpgradeDemo {
public static void main(String[] args) throws InterruptedException {
//为了看到完整的升级过程,我们"慢一点"
//1.创建一个对象,此时处于无锁状态(001)
Object lock = new Object();
//可以使用JOL工具打印对象头,这里用注释代替
System.err.println("====== 初始状态:无锁 ======\n" + ClassLayout.parseInstance(lock).toPrintable());
//2,主线程首先获取锁,触发偏向锁(101)
synchronized (lock) {
//此时对象头MarkWord中记录的是主线程的线程ID
System.err.println("====== 主线程持有锁:偏向锁 ======\n" + ClassLayout.parseInstance(lock).toPrintable());
}
//注意:这里锁被释放了,但对象头依然保留着主线程的ID(偏向状态)
//3,再创建一个新线程,来竞争这个锁
Thread t1 = new Thread(() -> {
synchronized (lock) {
//因为已经有偏向(偏向的是主线程),t1来竞争时,偏向锁撤销,升级为轻量级锁
//两个线程交替执行,没有同时竞争,所以是轻量级锁(自旋锁)
System.err.println("====== t1线程竞争锁:升级为轻量级锁 ======\n" + ClassLayout.parseInstance(lock).toPrintable());
}
});
t1.start();
t1.join();//等待t1结束
//4,创建大量线程进行激烈竞争,模拟升级为重量级锁
System.out.println("====== 开始激烈竞争,升级为重量级锁 ======");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
synchronized (lock) {
try {
//让线程持有锁一小段时间,增加竞争几率
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
//主线程也再次参与竞争
synchronized (lock) {
//在激烈竞争下,轻量级锁自旋失败,升级为重量级锁(010)
System.err.println("====== 主线程再次获取锁:此时已是重量级锁 ======\n" + ClassLayout.parseInstance(lock).toPrintable());
}
Thread.sleep(5000);//等待所有线程结束
System.out.println("程序结束");
}
}
代码执行过程分析
-
无锁(001)
- Object lock = new Object();执行后,lock对象处于无锁状态。对象头标记位为01。
-
偏向锁(101)
- 主线程进入第一个 synchronized(lock) 块。
- 由于开启了偏向锁且是第一个线程,JVM将对象标记为偏向模式,并将主线程的ID记录到MarkWord中。标记位变为101。
- 主线程退出同步块后,偏向状态依然保留。
-
轻量级锁(00)
- 线程t1启动并进入 synchronized(lock) 块。
- JVM发现对象已偏向于主线程,而当前线程是t1,于是撤销偏向锁。
- 由于此时只有主线程和t1两个线程在交替竞争(没有同时),竞争程度低,锁升级为轻量级锁。
- 轻量级锁的实现是在t1线程的栈帧中创建锁记录,并通过CAS操作将对象头指向该锁记录。标记位变为00。
-
重量级锁(10)
-
随后,我们启动了10个线程,它们几平同时去争夺lock锁。
-
轻量级锁的线程t1(或某个先获取锁的线程)在释放锁时,多个线程会通过CAS自旋来争抢。
-
当自旋超过一定次数(或等待线程超过一个),JVM认为竞争激烈,将锁升级为重量级锁。
-
此时,对象头指向操作系统级的互斥量(Mutex)。未能获取锁的线程会被挂起,进入阻塞队列。标记位变为10。
-
当主线程最后再次进入同步块时,它获取的就是一个重量级锁。
-
03、ReentrantLock与synchronized的性能对比及适用场景?
1.性能对比
在Java早期版本(如5.0之前),ReentrantLock 在性能上(尤其是在高竞争环境下)确实比 synchronized 有显著优势。但随着Java版本的迭代,synchronized 被不断优化(如引入了偏向锁、轻量级锁、锁消除、锁粗化等),两者的性能差距已经大大缩小。
在低竞争(线程争用不激烈)的场景下,synchronized 的性能甚至可能略优于 ReentrantLock,因为JVM可以对它进行深度优化,并且它是JVM的内置属性。
在高竞争(大量线程同时争抢锁)的场景下,ReentrantLock 通常能提供更稳定、可预测的吞吐量,因为它提供了更细粒度的控制,如公平锁选项。
2.适用场景
使用 synchronized 的场景:
- 开发简单性优先:代码简洁,不需要担心忘记释放锁,因为它会自动释放。
- 锁竞争不激烈:大部分业务场景,使用 synchronized 是首选,因为它足够快且不易出错。
- 与 wait() / notify() 机制配合:需要与对象的内置监视器进行交互时。
使用 ReentrantLock 的场景:
-
需要高级功能时:
-
可中断的锁获取:lockInterruptibly() 允许在等待锁的过程中响应中断。
-
超时获取锁:tryLock(long timeout,TimeUnit unit) 可以避免死锁或长时间等待。
-
尝试非阻塞获取锁:tryLock() 立即返回获取结果。
-
-
需要公平锁:要求线程按照申请锁的顺序来获取锁,防止某些线程永远无法获取到锁。
-
需要绑定多个条件:一个 ReentrantLock 可以绑定多个 Condition 对象,实现更精细的线程等待/唤醒。
-
高度竞争且性能是关键:在高并发场景下,通过 ReentrantLock 的灵活控制来优化吞吐量。
3.代码示例对比
-
场景1:使用 synchronized
javapublic class BankAccountSynchronized { private int balance; public BankAccountSynchronized(int balance) { this.balance = balance; } public synchronized int getBalance() { return balance; } //使用synchronized修饰方法,简洁安全 public synchronized void deposit(int amount) { balance += amount; System.out.println(Thread.currentThread().getName() + " 存款 " + amount + ",余额: " + balance); } public synchronized void withdraw(int amount) { if (balance >= amount) { balance -= amount; System.out.println(Thread.currentThread().getName() + " 取款 " + amount + ",余额: " + balance); } else { System.out.println(Thread.currentThread().getName() + " 取款失败,余额不足"); } } } -
场景2:使用 ReentrantLock(展示高级功能)
javaimport java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class BankAccountReentrantLock { private final Lock lock = new ReentrantLock();// 默认非公平锁 //创建一个与锁关联的条件对象,用于"余额足够"这个条件 private final Condition sufficientFunds = lock.newCondition(); private int balance; public BankAccountReentrantLock(int balance) { this.balance = balance; } public int getBalance() { lock.lock(); try { return balance; } finally { lock.unlock(); } } public void deposit(int amount) { lock.lock();//手动获取锁 try { balance += amount; System.out.println(Thread.currentThread().getName() + " 存款 " + amount + ",余额:" + balance); //存款后,通知所有等待"余额足够"条件的线程 sufficientFunds.signalAll(); } finally { lock.unlock();//必须在finally块中释放锁,确保锁被释放 } } public void withdraw(int amount) throws InterruptedException { lock.lock(); try { //使用while循环防止"虚假唤醒" while (balance < amount) { System.out.println(Thread.currentThread().getName() + " 余额不足,请等待..."); //等待存款操作唤醒。这个方法可以响应中断! sufficientFunds.await(); } balance -= amount; System.out.println(Thread.currentThread().getName() + " 取款 " + amount + ",余额: " + balance); } finally { lock.unlock(); } } //展示tryLock的用法:尝试转账,如果失败不阻塞 public boolean tryTransfer(BankAccountReentrantLock to, int amount) { //尝试获取本账户的锁 if (this.lock.tryLock()) { try { //为了演示,这里休眠一下,增加竞争机会 Thread.sleep(100); //尝试获取目标账户的锁 if (to.lock.tryLock()) { try { if (this.balance >= amount) { this.balance -= amount; to.balance += amount; System.out.println("转账成功!"); return true; } } finally { to.lock.unlock(); } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { this.lock.unlock(); } } System.out.println("转账失败,无法获取所有锁。"); return false; } } -
代码分析:
- 简洁性:synchronized 版本明显更简洁。ReentrantLock 必须在 finally 块中手动释放锁,否则会导致死锁。
- 功能性:
- ReentrantLock 版本实现了条件等待。当取款时余额不足,线程会调用 await() 释放锁并等待,直到被存款线程 signalAll() 唤醒。这比 synchronized 配合 wait() /notify() 更安全、更直观(因为一个锁可以关联多个条件)。
- tryTransfer 方法展示了 tryLock() 的强大之处。它尝试获取两个账户的锁,如果无法同时获取,它会立即释放已获得的锁并返回false,从而避免了死锁的风险。这是 synchronized 无法直接实现的。
-
面试回答建议:首先说明两者都是可重入锁。然后从性能演变(早期差距大,现在缩小)和功能差异两个角度切入。强 synchronized 的简洁性和通用性,以及 ReentrantLock 的高级功能。最后结合代码,重点解释 ReentrantLock 在解决复杂问题(如条件队列、尝试非阻塞获取锁)时的优势。这样回答既全面又有深度。
04、ReentrantLock的公平锁/非公平锁实现原理?
1.非公平锁的实现
非公平锁是ReentrantLock的默认实现。
加锁过程
1、lock() 方法
原理:新线程到来时,它不会去看队列里是否有人在排队,而是直接尝试通过CAS操作去获取锁。这是一种"插队"行为,破坏了公平性。
java
// NonfairSync 类
final void lock() {
//关键点1:不管三七二十一,先尝试直接抢锁一次
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
} else {
acquire(1);//如果没抢到,再走正常的AQS获取流程
}
}
2、acquire(int) 方法(AQS中的方法)
原理:这个方法会先调用tryAcquire再次尝试获取锁,如果失败,才会将线程放入等待队列。
java
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
selfInterrupt();
}
}
3、tryAcquire(int) 方法(非公平锁的实现)
原理:在tryAcquire中,当它发现锁状态state为0(锁可用)时,它依然不会去检查等待队列,而是直接进行CAS操作尝试获取锁。这给了新线程第二次"插队"的机会。
java
// NonfairSync 类
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
//Sync 类中的方法
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//关键点2:即使现在锁空闲了,它也不检查队列,再次尝试CAS抢锁
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;
}
非公平锁流程总结:
一个新线程在获取锁时,有两次机会可以"插队":
- 在 lock() 方法中,直接CAS抢锁。
- 在tryAcquire方法中,如果发现锁被释放,再次直接CAS抢锁。
只有这两次都失败了,它才会乖乖地进入等待队列排队。这极大地提高了吞吐量,因为避免了线程挂起和唤醒的开销,但可能导致队列中的线程"饥饿"。
2.公平锁的实现
公平锁的实现则严格遵守FIFO原则。
加锁过程
1、lock() 方法
原理:公平锁的 lock() 方法非常"守规矩",直接调用 acquire() ,没有任何额外的CAS尝试。
java
// FairSync 类
final void lock() {
acquire(1);//直接调用AQS的acquire,没有"插队"逻辑
}
2、acquire(int) 方法(AQS中的方法)
原理:和非公平锁一样,调用路径是:acquire -> tryAcquire -> (失败则入队)。
3、tryAcquire(int) 方法(公平锁的实现)
java
// FairSync 类
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//关键区别:在尝试CAS获取锁之前,先检查是否有前辈在排队
if (!hasQueuedPredecessors() && //就是这个方法!
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
//重入逻辑,和非公平锁一样
int nextc = c + acquires;
if (nextc < 0) {
throw new Error("Maximum lock count exceeded");
}
setState(nextc);
return true;
}
return false;
}
4、hasQueuedPredecessors() 方法(AQS中的方法)
原理:这个方法检查等待队列中是否存在有效的、非当前线程的节点。
- h != t :判断队列是否不为空(头节点不等于尾节点)。
- (s=h.next) == null :判断头节点的下一个节点是否为空(一种并发下的边缘情况)。
- s.thread != Thread.currentThread() :判断头节点的下一个节点(即等待时间最长的线程)是不是当前线程自己。
只有当 hasQueuedPredecessors() 返回 false(即:队列为空,或者当前线程就是队首的线程)时,当前线程才有资格去尝试CAS获取锁。
java
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
//返回true表示队列中有其他线程排在我前面
return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}
公平锁流程总结:
一个新线程在获取锁时,必须遵守规则:
- 当锁可用时(state==0),它必须先调用 hasQueuedPredecessors() 检查队列。
- 只有在确认自己是队列的头节点(或者队列为空)时,它才被允许去尝试CAS获取锁。
- 否则,它必须直接进入队列尾部等待。
05、ReadWriteLock和StampedLock的优化场景?
1.ReadWriteLock的优化场景
优化场景:典型的"读多写少"场景。例如,缓存系统、配置信息存储等,其中数据的读取操作频率远高于写入操作。
核心机制:
- 共享读锁:多个线程可以同时持有读锁。
- 独占写锁:写锁是独占的,当有线程持有写锁时,其他所有读锁和写锁请求都会被阻塞。反之,当有线程持有读锁时,写锁请求会被阻塞。
优化点分析:
- 当多个线程(例如100个)同时调用get方法时,它们可以同时获得读锁并访问缓存,并发性能远高于使用synchronized对整个方法加锁。
- 只有当put操作发生时,才会短暂地阻塞所有读和写,保证数据的一致性。
潜在问题:
- 写线程饥饿:如果读线程源源不断,写线程可能永远无法获得锁(在非公平锁策略下)。
- 锁降级:支持从写锁降级为读锁,但不支持锁升级(从读锁升级为写锁),容易导致死锁。
代码示例:一个简单的缓存
java
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockCache<K, V> {
private final Map<K, V> cache = new HashMap<>();
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
//写操作:使用写锁
public void put(K key, V value) {
rwLock.writeLock().lock();//获取写锁,独占
try {
cache.put(key, value);
} finally {
rwLock.writeLock().unlock();
}
}
//读操作:使用读锁
public V get(K key) {
rwLock.readLock().lock();//获取读锁,共享
try {
return cache.get(key);
} finally {
rwLock.readLock().unlock();
}
}
}
2.StampedLock的优化场景
优化场景:在"读非常多,写极少"的场景下,追求比ReadWriteLock更高的性能。它通过"乐观读"机制,几乎完全消除了读操作在无竞争时的锁开销。
核心机制:
- 写锁:与ReadWriteLock的写锁类似,是独占的。
- 悲观读锁:与ReadWriteLock的读锁类似。
- 乐观读:这是其最大的亮点。线程在读数据时,不直接获取锁,而是获得一个"邮票"(stamP)。在读完数据后,需要验证这个stamp是否仍然有效(即在此期间没有发生过写操作)。如果有效,说明读到的数据是一致的;如果无效,则可以重试或升级为悲观读锁。
优化点分析:
- 在绝大多数没有写操作发生的时刻,get方法会走完第1、2、5步。它只在最后调用了一个无竞争的 validate() 方法,而完全没有执行lock() 和 unlock() 操作。这在极高并发读的场景下,性能提升是巨大的。
- 只有在极少数情况下(刚好在读的过程中发生了写操作),才会升级为获取一个悲观的读锁,重新读取数据。这种"乐观"的假设在"写极少"的场景下成功率很高。
- StampedLock 的锁申请和释放通常都比 ReadWriteLock 更快。
代码示例:使用StampedLock优化上述缓存
java
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.StampedLock;
public class StampedLockCache<K, V> {
private final Map<K, V> cache = new HashMap<>();
private final StampedLock stampedLock = new StampedLock();
//写操作:与ReadWriteLock类似
public void put(K key, V value) {
long stamp = stampedLock.writeLock();// 获取写锁
try {
cache.put(key, value);
} finally {
stampedLock.unlockWrite(stamp);
}
}
//读操作:使用乐观读进行优化
public V get(K key) {
//1.尝试乐观读
long stamp = stampedLock.tryOptimisticRead();
//2,读取数据(此时并没有持有锁!)
V value = cache.get(key);
//3,验证在读取过程中,是否有写操作发生
if (!stampedLock.validate(stamp)) {
//4,如果验证失败,说明有写操作,升级为悲观读锁
stamp = stampedLock.readLock();
try {
value = cache.get(key);
} finally {
stampedLock.unlockRead(stamp);
}
}
//5,如果验证成功,直接返回数据,避免了锁的开销
return value;
}
}
06、volatile关键字的可见性与禁止指令重排序原理?
1.可见性
原理
- 一个变量被声明为volatile时,任何线程对该变量的修改都会立即刷新到主内存。
- 其他线程读取该变量时,会强制从主内存重新读取最新值。
- 这解决了多线程环境下的内存可见性问题。
代码示例
java
public class VisibilityDemo {
//不使用volatile,可能出现可见性问题
private static boolean flagWithoutVolatile = false;
//使用volatile,保证可见性
private static volatile boolean flagWithVolatile = false;
public static void main(String[] args) throws InterruptedException {
//测试无volatile的情况
Thread thread1 = new Thread(() -> {
while (!flagWithoutVolatile) {
//空循环,等待flag变为true
}
System.out.println("Thread1: 检测到flagWithoutVolatile变为true");
});
Thread thread2 = new Thread(() -> {
try {
Thread.sleep(1000);// 等待1秒
flagWithoutVolatile = true;
System.out.println("Thread2: 已将flagWithoutVolatile设置为true");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread1.start();
thread2.start();
// thread1.join();
// thread2.join();
System.out.println("--- 测试volatile版本---");
//测试有 volatile 的情况
Thread thread3 = new Thread(() -> {
while (!flagWithVolatile) {
//空循环,等待flag变为true
}
System.out.println("Thread3: 检测到flagWithVolatile变为true");
});
Thread thread4 = new Thread(() -> {
try {
Thread.sleep(1000);// 等待1秒
flagWithVolatile = true;
System.out.println("Thread4: 已将flagWithVolatile设置为true");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread3.start();
thread4.start();
thread3.join();
thread4.join();
}
}
/*
--- 测试volatile版本---
Thread4: 已将flagWithVolatile设置为true
Thread3: 检测到flagWithVolatile变为true
Thread2: 已将flagWithoutVolatile设置为true
*/
2.禁止指令重排序
原理
- 编译器和处理器为了优化性能,可能会对指令进行重排序。
- volatile通过内存屏障(MemoryBarrier)来禁止指令重排序。
- 遵循happens-before原则,确保volatile写操作前的所有操作不会被重排序到写操作之后。
双重检查锁定单例模式(最经典的应用)
java
public class Singleton {
//必须使用volatile,防止指令重排序
private static volatile Singleton instance;
private Singleton() {
System.out.println("singleton实例被创建");
}
public static Singleton getInstance() {
if (instance == null) {//第一次检查
synchronized (Singleton.class) {// 加锁
if (instance == null) {//第二次检查
instance = new Singleton();//如果没有volatile,这里可能发生重排序
}
}
}
return instance;
}
}
指令重排序问题分析
java
//看似简单的实例化操作,实际上包含三个步骤:
instance = new Singleton();
//实际执行过程:
// 1. memory = allocate();分配内存空间
// 2. initInstance(memory);//初始化对象
// 3. instance = memory;//将instance指向分配的内存地址
// 如果没有volatile,可能被重排序为:
// 1. memory = allocate();//分配内存空间
// 3. instance = memory;//将instance指向分配的内存地址(此时对象还未初始化!)
// 2. initInstance(memory);//初始化对象
//这样其他线程可能拿到一个未完全初始化的对象
07、volatile为何不能保证原子性?
核心结论
volatile关键字能保证可见性和有序性,但不能保证原子性。
- 可见性:当一个线程修改了volatile变量,新值会立即被刷新到主内存,并强制其他线程的工作内存中该变量的缓存失效,从而使其他线程在读取时能拿到最新的值。
- 有序性:禁止指令重排序,确保volatile变量读写操作前后的指令不会被打乱。
- 原子性缺失:volatile无法保证对一个变量的复合操作是原子性的。一个复合操作通常包含多个步骤(例如:读-改-写),如果在这些步骤执行过程中线程被切换,就会导致数据不一致。
为什么不能保证原子性?以i++为例
i++这个看似简单的操作,在JVM层面其实分为三个步骤:
- Read:从主内存读取变量1的当前值到线程的工作内存。
- Modify:在工作内存中将1的值加1。
- Write:将工作内存中新的i值写回主内存。
问题所在:假设i是一个volatile变量,初始值为0。当两个线程A和B几乎同时执行i++时,可能会发生以下情况:
- 都读取 i=0 到工作内存,在工作内存中执行 i+1(结果为1),都将1写回主内存。
- 最终结果,i的值是1,而不是预期的2。两个线程各做了一次i++,但最终只增加了1。
volatile在这里的作用:
- 它保证了在t5时刻,线程B将值1写回主内存后,会立即使得线程A工作内存中的i的缓存失效。
- 但是,线程A的"改"操作已经完成了(它已经在自己的工作内存中把0变成了1)。所以当它恢复执行时,它只是简单地将这个过期的计算结果1写回了主内存,覆盖了线程B的结果。
代码演示
下面的代码将创建一个多线程环境,对volatile变量进行自增,观察最终结果是否与预期相符。
运行结果分析:你每次运行这段代码,得到的结果都会不同,但几乎总是远小于1,000,000。这正证明了volatile无法保证count++这个复合操作的原子性。
java
public class VolatileAtomicityDemo {
//使用volatile修饰计数器
private volatile int count = 0;
//增加的方法
public void increase() {
count++;//这是一个非原子操作
}
public static void main(String[] args) throws InterruptedException {
final VolatileAtomicityDemo demo = new VolatileAtomicityDemo();
//创建1000个线程,每个线程对count自增1000次
int threadCount = 1000;
int incrementsPerThread = 1000;
Thread[] threads = new Thread[threadCount];
for (int i = 0; i < threadCount; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < incrementsPerThread; j++) {
demo.increase();
}
});
threads[i].start();
}
//等待所有线程执行完毕
for (Thread t : threads) {
t.join();
}
//预期结果:1000*1000=1,000,000
//实际结果:几乎总是小于1,000,000
System.out.println("预期结果:" + (threadCount * incrementsPerThread));
System.out.println("实际结果:" + demo.count);
}
}
如何保证原子性?
-
要解决这个问题,需要让 increase() 方法成为原子操作。使用synchronized关键字。synchronized可以保证同一时刻只有一个线程能执行这个方法,从而将"读-改-写"操作打包成一个不可分割的原子操作。
javapublic synchronized void increase() { count++; } -
使用java.util.concurrent.atomic包下的原子类。AtomicInteger 的 incrementAndGet() 方法底层使用了CAS(Compare-And-
Swap)操作,它是一种无锁的原子操作,性能通常比synchronized更高,是现代并发编程的首选。
java//使用 AtomicInteger 代替 volatile int private AtomicInteger count = new AtomicInteger(0); //增加的方法 public void increase() { count.incrementAndGet();//这是一个原子操作 }
08、CAS的底层实现?
核心概念
CAS(Compare-And-Swap),即比较并交换,是一种无锁的、乐观的并发控制机制。它的操作是原子的,意味着在执行过程中不会被其他线程中断。
它的核心思想是:我认为V的值应该是A,如果是,那么把它更新为B;如果不是,说明有其他线程修改过了,那我就不更新了,并可以选择重试或放弃。
Java中的体现
在Java中,我们通常不直接操作底层指令,而是通过 java.util.concurrent.atomic 包下的原子类(如AtomicInteger)来使用CAS。这些类的compareAndSet、 getAndInc rement等方法内部都依赖于CAS操作。
底层实现剖析
CAS的底层实现可以分为三个层次:Java代码->JVM本地方法->硬件CPU指令。
-
Java代码层(应用层):这是我们直接编写和调用的层面。
javaimport java.util.concurrent.atomic.AtomicInteger; public class CASDemo { public static void main(String[] args) { AtomicInteger atomicInt = new AtomicInteger(5);// 初始值为5 //这就是一个典型的CAS操作 //期望值是5,如果当前值确实是5,就把它更新为10 boolean success = atomicInt.compareAndSet(5, 10); System.out.println("更新是否成功:" + success + ",当前值:" + atomicInt.get()); //这次会失败,因为当前值已经是10,而不是8 success = atomicInt.compareAndSet(8, 15); System.out.println("更新是否成功::" + success + ",当前值:" + atomicInt.get()); } } -
JVM本地方法层(JNI层)
当我们点进 AtomicInteger.compareAndSet 方法,会发现它调用了 Unsafe 类的 compareAndSwapInt 方法。
javapublic final boolean compareAndSet(int expectedValue, int newValue) { return U.compareAndSetInt(this, VALUE, expectedValue, newValue); }这个 Unsafe类是JDK内部使用的、用于执行低级别、不安全操作的工具类。它提供了像访问内存、操作对象字段偏移量等直接功能。compareAndSwapInt就是一个本地方法(Native Method),它的实现不在Java中,而在C/C++代码中。
javapublic final native boolean compareAndSetInt(Object o, long offset, int expected, int x); -
操作系统/硬件层(最终实现)
这是CAS魔法的发生地。JVM会为这个本地方法实现不同的版本,具体取决于操作系统和CPU架构。
-
在x86架构的Linux系统上:这个本地方法最终会映射到CPU的一条名为 CMPXCHG(Compare and Exchange) 的汇编指令。
-
在其他平台:可能会是其他不同的指令,但原理相似。
CMPXCHG指令的工作流程:
这条指令会原子性地执行以下步骤:
- 将指定内存位置的值与某个寄存器(如EAX)中的值(期望值)进行比较。
- 如果相等,则将另一个寄存器中的值(新值)写入该内存位置,并设置标志位(如ZF)为1表示成功。
- 如果不相等,则不进行写操作,并将标志位清0表示失败。
因为这是一条CPU指令,所以它在执行期间会锁定CPU缓存行或总线,确保整个"比较-交换"操作是不可分割的原子操作。这是硬件级别的保证,效率远高于软件级别的锁。
-
09、ABA问题是什么?如何解决?
ABA问题是什么?
ABA问题是在并发编程中使用乐观锁(如CAS操作)时可能遇到的一个经典问题。它发生在以下场景:
- 线程1读取共享变量的值为A。
- 线程1被挂起,线程2开始执行。
- 线程2将共享变量的值从A改为B,然后又改回A。
- 线程1恢复执行,使用CAS检查发现值仍然是A,于是操作成功。
虽然最终值看起来没有变化,但实际上中间经历了A→B→A的过程,这可能导致数据不一致的问题。
解决方案
-
使用AtomicStampedReference
AtomicStampedReference 通过添加版本戳(stamp)来解决ABA问题。
javaimport java.util.concurrent.atomic.AtomicStampedReference; public class ABAProblemSolution { private static AtomicStampedReference<Integer> atomicStampedValue = new AtomicStampedReference<>(100, 0); public static void main(String[] args) throws InterruptedException { //初始值:100,版本号:0 System.out.println("初始值:" + atomicStampedValue.getReference() + ",版本号:" + atomicStampedValue.getStamp()); Thread thread1 = new Thread(() -> { int[] stampHolder = new int[1]; Integer currentValue = atomicStampedValue.get(stampHolder); int currentStamp = stampHolder[0]; System.out.println("线程1 读取-值:" + currentValue + ",版本号:" + currentStamp); //模拟线程1被阻塞 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //尝试CAS操作 boolean success = atomicStampedValue.compareAndSet( currentValue, 200, currentStamp, currentStamp + 1); System.out.println("线程1 CAS操作结果:" + success + ",当前值:" + atomicStampedValue.getReference() + ",版本号:" + atomicStampedValue.getStamp()); }); Thread thread2 = new Thread(() -> { int[] stampHolder = new int[1]; Integer currentValue = atomicStampedValue.get(stampHolder); int currentStamp = stampHolder[0]; boolean success1 = atomicStampedValue.compareAndSet( currentValue, 150, currentStamp, currentStamp + 1); System.out.println("线程2 currentValue=" + currentValue + " currentStamp=" + currentStamp + ",第一次CAS操作结果:" + success1 + ",当前值:" + atomicStampedValue.getReference() + ",版本号:" + atomicStampedValue.getStamp()); currentValue = atomicStampedValue.get(stampHolder); currentStamp = stampHolder[0]; boolean success2 = atomicStampedValue.compareAndSet( currentValue, 100, currentStamp, currentStamp + 1); System.out.println("线程2 currentValue=" + currentValue + " currentStamp=" + currentStamp + ",第二次CAS操作结果:" + success2 + ",当前值:" + atomicStampedValue.getReference() + ",版本号:" + atomicStampedValue.getStamp()); }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); } } /* 初始值:100,版本号:0 线程1 读取-值:100,版本号:0 线程2 currentValue=100 currentStamp=0,第一次CAS操作结果:true,当前值:150,版本号:1 线程2 currentValue=150 currentStamp=1,第二次CAS操作结果:true,当前值:100,版本号:2 线程1 CAS操作结果:false,当前值:100,版本号:2 */ -
使用AtomicMarkableReference
AtomicMarkableReference使用布尔标记而不是版本号,适用于某些简单场景。
javaimport java.util.concurrent.atomic.AtomicMarkableReference; public class ABASolutionWithMarkable { private static AtomicMarkableReference<Integer> atomicMarkableValue = new AtomicMarkableReference<>(100, false); public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(() -> { boolean[] markHolder = new boolean[1]; Integer currentValue = atomicMarkableValue.get(markHolder); boolean currentMark = markHolder[0]; System.out.println("线程1 读取-值:" + currentValue + ",标记: " + currentMark); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } boolean success = atomicMarkableValue.compareAndSet( currentValue, 200, currentMark, !currentMark); System.out.println("线程1 CAS操作结果:" + success + " currentValue:" + 200 + " currentMark:" + currentMark + "->" + !currentMark); }); Thread thread2 = new Thread(() -> { boolean[] markHolder = new boolean[1]; Integer currentValue = atomicMarkableValue.get(markHolder); boolean currentMark = markHolder[0]; //修改为150 boolean success1 = atomicMarkableValue.compareAndSet(currentValue, 150, currentMark, !currentMark); System.out.println("线程2 CAS操作结果:" + success1 + " currentValue:" + 150 + " currentMark:" + currentMark + "->" + !currentMark); //再修改回100 currentValue = atomicMarkableValue.get(markHolder); currentMark = markHolder[0]; boolean success2 = atomicMarkableValue.compareAndSet(currentValue, 100, currentMark, !currentMark); System.out.println("线程2 CAS操作结果:" + success2 + " currentValue:" + 100 + " currentMark:" + currentMark + "->" + !currentMark); }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); } } /* 线程1 读取-值:100,标记: false 线程2 CAS操作结果:true currentValue:150 currentMark:false->true 线程2 CAS操作结果:true currentValue:100 currentMark:true->false 线程1 CAS操作结果:true currentValue:200 currentMark:false->true */
总结
-
ABA问题:在CAS操作中,值从A变为B又变回A,导致CAS误认为值未被修改。
-
解决方案:
- AtomicStampedReference:使用版本号戳记
- AtomicMarkableReference:使用布尔标记
- 两种方案都能有效检测到中间的状态变化
-
适用场景:在需要严格保证状态连续性的并发场景中使用。
通过使用带版本号的原子引用类,我们可以有效避免ABA问题,确保并发操作的正确性。
10、锁消除和锁粗化的JVM优化策略?
1.锁消除
核心思想:JVM在JIT编译时,通过逃逸分析技术,判断出在某些代码段中,一个锁对象不可能被其他线程访问到(即不会"逃逸"出当前线程),那么即使代码中显式地写了同步操作,JVM也会自动将这些同步锁消除掉,从而提升性能。
逃逸分析:分析对象的动态作用域,判断一个对象是否会被外部方法或外部线程所引用。
- 如果对象只在方法内部使用,则认为是方法逃逸。
- 如果对象被其他线程访问,则认为是线程逃逸。
- 如果对象没有逃逸,那么它就是线程安全的,许多优化(如栈上分配、标量替换、锁消除)就可以进行。
代码示例:
java
public class LockEliminationExample {
//这个StringBuffer是方法内的局部变量,并且没有逃逸出方法
public String createString(int a, int b) {
//StringBuffer是线程安全的,因为它的关键方法都是synchronized的
StringBuffer sb = new StringBuffer();
sb.append(a);
sb.append(b);
return sb.toString();//注意:这里返回的是String,不是StringBuffer本身
}
}
分析过程:
- sb是一个在 createString 方法内部创建的局部变量。
- 这个变量没有作为参数传递给其他方法(这里的append和toString)调用,接收者是sb本身,但(sb的引用没有被传递出去)。
- 方法返回的是 sb.toString() 得到的全新的String对象,而不是StringBuffer对象本身。因此,sb这个对象的引用没有逃逸出createString 方法。
- 由于sb是一个线程私有的、不可能被其他线程访问到的对象,那么对它进行的同步(StringBuffer 的append 方法是synchronized 的)就完全是多余的。
JVM的优化:在JIT编译器(主要是C2编译器)运行时,通过逃逸分析确认了sb对象不会逃逸。因此,它会将StringBuffer的synchronized锁直接消除掉。最终,这段代码的执行效率和使用非线程安全的StringBuilder是一样的。
2.锁粗化
核心思想:如果一系列的连续操作都是对同一个锁对象进行反复的加锁和解锁,即使这些操作出现在不同的代码块中,JVM也会将这些零散的同步操作"粗化"为一个更大的同步块,从而减少锁的获取和释放次数,极大地提升性能。
代码示例:
java
public class LockCoarseningExample {
private final Object lock = new Object();
public void doSomething() {
//在循环中反复加锁解锁,性能很低
for (int i = 0; i < 100; i++) {
synchronized (lock) {
//执行一些简单的操作,例如赋值
System.out.println(i);
}
}
}
}
分析过程:
- 在doSomething方法的循环中,每次迭代都会进入一次 synchronized(lock) 块,然后退出。
- 这意味着在100次循环中,会发生100次锁获取和100次锁释放。
- 锁的获取和释放本身是有开销的(涉及用户态/内核态切换、CAS操作等),频繁的锁竞争会严重损害性能。
JVM的优化:JVM的JIT编译器会探测到这种在循环中对同一个锁的密集操作。为了优化,它会将锁的范围粗化到整个循环的外面。
优化后的等效代码:这样,锁只需要获取和释放一次,性能得到了巨大提升。
java
public void doSomethingOptimized() {
//JVM将锁粗化到循环外部,只需要加锁一次
synchronized (lock) {
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
}
}
更复杂的场景:锁粗化不仅仅发生在循环中。只要JVM检测到一连串的"加锁-解锁-加锁-解锁"操作是针对同一个锁,并且相邻操作之间没有复杂的、耗时的、或可能引起阻塞的代码,它就可能会进行粗化。在这种情况下,JVM很可能将操作1和操作2所在的同步块合并成一个大的同步块。
java
public void multipleSyncBlocks() {
synchronized (lock) {
//操作1
}
//这里只有一些非常简单的指令,比如一个简单的赋值或方法调用
doSomethingSimple();
synchronized (lock) {
//操作2
}
//可能还有更多的synchronized块
}