面试整理——Java并发编程

Java并发编程

一. 多线程

1.1 线程

问:什么时候用线程?

  • 需要频繁创建和销毁
  • 需要频繁切换
  • 多核环境
  • 并行操作
  • 相比稳定安全更需要速度

问:创建多线程的方式,以及线程的状态转换?⭐⭐⭐

Java中创建多线程的方式有:

  • 继承Thread类,重写run(),创建线程对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class MyThread extends Thread {
    @Override
    public void run() {
    System.out.println("Thread is running");
    }
    }

    public class Main {
    public static void main(String[] args) {
    MyThread thread = new MyThread();
    thread.start(); // 启动线程
    }
    }
  • 实现Runnable接口,重写run(),创建线程对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class MyRunnable implements Runnable {
    @Override
    public void run() {
    System.out.println("Runnable is running");
    }
    }

    public class Main {
    public static void main(String[] args) {
    Thread thread = new Thread(new MyRunnable());
    thread.start(); // 启动线程
    }
    }
  • 实现Callable接口,重写call(),创建实例对象并作为构造器参数创建FutureTask对象,再使用FutureTask对象作为构造器参数创建线程对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
    return "Callable result";
    }
    }

    public class Main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
    FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
    Thread thread = new Thread(futureTask);
    thread.start();
    System.out.println(futureTask.get()); // 获取线程返回值
    }
    }
  • 使用Executor框架创建线程池。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class Main {
    public static void main(String[] args) {
    // 创建固定大小为 3 的线程池
    ExecutorService executor = Executors.newFixedThreadPool(3);

    for (int i = 0; i < 5; i++) {
    executor.execute(() -> {
    System.out.println("Thread: " + Thread.currentThread().getName() + " is running");
    });
    }

    // 关闭线程池
    executor.shutdown();
    }
    }

线程状态

状态 名称 示例 介绍
New 初始状态 new Thread(runnable) 线程还未开始运行,处于创建状态
Runnable 运行状态 thread.start() 可运行但未必在运行,取决于OS分配的运行时间,即就绪和运行两状态的合并。
Blocked 阻塞状态 请求内部的对象锁,但锁被其他线程持有
Waiting 等待状态 Object.wait() / Thread.join() / 等待Lock或Condition 当线程等待另一个线程通知调度器某个条件时,处于此状态
Timed waiting 超时等待 Object.wait() / Thread.join() / Thread.sleep() / Lock.tryLock() / Condition.await() 调用有些有超时参数的方法时,可以指定时间自行返回
Terminated 终止状态 不要使用stop() 要么因为run方法正常结束而终止,要么因为没有捕获的异常终止了run方法而结束

一个经历了所有状态的线程可能会经历以下流程:

  1. 线程创建后调用start()方法开始,状态由NEW->RUNNABLE。
  2. 当线程执行wait()、join()或 sleep()后,线程进入WAITING/TIME_WAITING状态。处于WAITING状态的线程需要等待其他线程的通知才可以恢复RUNNABLE状态,而TIME_WAITING则是在WAITING上增加了时间限制,在超时后会自动返回RUNNABLE状态。
  3. 当线程调用同步方法时,在无法获得锁的情况下,线程进入BLOCKED状态。线程在执行run()方法后将进入终止状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class ThreadStateDemo {

public static void main(String[] args) throws InterruptedException {

// 创建线程
Thread thread = new Thread(new Task());
System.out.println("1. Thread state after creation: " + thread.getState()); // NEW

// 启动线程
thread.start();
System.out.println("2. Thread state after calling start(): " + thread.getState()); // RUNNABLE

// 主线程睡眠 1 秒,让子线程进入睡眠状态
Thread.sleep(1000);
System.out.println("3. Thread state while sleeping (TIMED_WAITING): " + thread.getState());

// 主线程等待子线程唤醒,确保子线程进入 WAITING 状态
Thread.sleep(3000);
System.out.println("4. Thread state while waiting for lock (WAITING): " + thread.getState());

// 唤醒子线程
synchronized (Task.class) {
Task.class.notify();
}

// 主线程等待子线程完成
thread.join();
System.out.println("5. Thread state after completion: " + thread.getState()); // TERMINATED
}

static class Task implements Runnable {
@Override
public void run() {
try {
// TIMED_WAITING:线程在这里睡眠
Thread.sleep(2000);

synchronized (Task.class) {
System.out.println("Acquired lock, entering WAITING state...");
Task.class.wait(); // WAITING:线程等待锁被唤醒
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}

// 打印:
1. Thread state after creation: NEW
2. Thread state after calling start(): RUNNABLE
3. Thread state while sleeping (TIMED_WAITING): TIMED_WAITING
4. Thread state while waiting for lock (WAITING): WAITING
5. Thread state after completion: TERMINATED

问:终止线程的方法?

  1. 使用退出标志/共享标志位(如volatile声明的变量):线程周期性地检查该标志变量的状态,决定是否退出。
  2. 通过interrupt()中断:
    • 不会直接停止线程,而是通过设置线程的中断标志,通知线程它应该停止工作。线程内run()通过isInterrupted()判断是否中断,若处于阻塞状态就无法检测中断状态,会抛出异常InterruptedException。
    • future.cancel(true)+future.isCancelled(),本质上仍是FutureTask通过interrupt()中断。
    • ExecutorService.shutdown(),本质上仍是ThreadPoolExecutor通过interrupt()中断。
  3. Thread.stop()强制终止,但会导致资源释放问题、锁泄露以及数据不一致。
  4. 守护线程:将线程设置为守护线程,它会在所有非守护线程结束时自动终止。

问:谈一下线程切换,并引申到Java阻塞,运行?

  • 什么是线程切换?
    • 线程切换就是指CPU从一个进程/线程切换到另一个进程/线程
    • 大概过程可以简单的理解为:(上下文指某个时间节 点CPU寄存器和计数器的内容)。
      • 挂起当前线程(存储当前上下文),
      • 恢复一个线程(找到一个合适的上下文并恢复到寄存器),
      • 跳转到程序计数器指向的位置(即线程被中断时的代码行)。
  • 线程切换的触发场景?
    • 时间片耗尽:操作系统的调度策略,如时间片轮转。
    • IO操作:线程进行IO操作会进入阻塞状态,调度器切换其它线程。
    • 锁竞争:多线程竞争锁资源,未获取锁的线程进入等待状态,引起线程切换。
    • 手动yield或sleep:Thread.yield()或Thread.sleep()。
  • Java 中的阻塞与运行:
    • 阻塞就是指线程执行到某一阶段时,需要获取某些资源才能继续执行,但此时这些资源被其他线程占用,所以当前线程需要处于等待状态。常见的阻塞原因:等待锁、IO操作、等待通知等。
    • 运行状态表示线程正在被CPU执行。当阻塞状态消失(如锁被释放或IO完成)时,线程有机会从阻塞状态切换回可执行状态(RUNNABLE),等待被调度器分配CPU资源。

问:线程的中断机制?

  • Java的线程中有一个中断标识位,表示是否有中断请求,并在Thread中提供了 interrupt() 来中断线程,以及 interrupted()isInterrupted() 来判断当前中断状态,前者会将中断复位。
  • 中断通常在阻塞操作上触发,比如 Thread.sleep()wait()join() 等。如果线程在这些方法上收到中断请求,会抛出 InterruptedException,此时线程可选择是否响应中断,或者继续执行。
  • 中断并不意味着线程会立即终止,中断线程可以任意处理,通常会把中断请求当作终止请求,由线程根据情况在合适的时机终止。
  • 处于阻塞状态的线程会抛出异常InterruptedException,且抛出异常的方法大多会先重置中断标识位再抛出异常。

问:怎么实现所有线程在等待某个事件的发生才会去执行?⭐⭐

  • 读写锁:主线程先获取写锁,所有子线程获取读锁,等事件发生时主线程释放写锁。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    import java.util.concurrent.locks.ReadWriteLock;
    import java.util.concurrent.locks.ReentrantReadWriteLock;
    // 写锁 保证数据加载时没有其他线程能访问 data。
    // 读锁 让所有读线程在数据加载完成后可以安全读取 data,不影响彼此
    public class ReadWriteLockExample {
    private static final ReadWriteLock lock = new ReentrantReadWriteLock();
    private static String data = "Initial Data";

    public static void main(String[] args) throws InterruptedException {
    // 写线程:模拟加载数据
    new Thread(() -> {
    lock.writeLock().lock();
    try {
    System.out.println("Writing data...");
    Thread.sleep(2000); // 模拟数据加载
    data = "Loaded Data";
    System.out.println("Data loaded.");
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    } finally {
    lock.writeLock().unlock();
    }
    }).start();

    // 读线程:等待数据加载完成后再读取
    for (int i = 0; i < 5; i++) {
    new Thread(() -> {
    lock.readLock().lock();
    try {
    System.out.println(Thread.currentThread().getName() + " reads: " + data);
    } finally {
    lock.readLock().unlock();
    }
    }).start();
    }
    }
    }
  • 信号量-Semaphore:控制一定数量的线程同时访问某个资源或执行某个任务。初始值设为N,主线程先调用 acquire(N) 申请N个信号量,其他线程调用 acquire() 阻塞等待,等事件发生时主线程同时释放N个信号量。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    import java.util.concurrent.Semaphore;
    // semaphore.acquire() 让线程等待信号。
    // 主线程完成准备后调用 semaphore.release(threadCount),释放信号量,允许所有线程执行。
    public class SemaphoreExample {
    private static final Semaphore semaphore = new Semaphore(0); // 0表示初始所有线程阻塞

    public static void main(String[] args) throws InterruptedException {
    int threadCount = 5;

    // 启动5个线程等待信号量
    for (int i = 0; i < threadCount; i++) {
    new Thread(new Task(semaphore)).start();
    }

    System.out.println("Main thread preparing...");
    Thread.sleep(2000); // 模拟准备时间

    System.out.println("Main thread releases all threads.");
    semaphore.release(threadCount); // 释放信号量,允许所有线程执行
    }
    }

    class Task implements Runnable {
    private final Semaphore semaphore;

    public Task(Semaphore semaphore) {
    this.semaphore = semaphore;
    }

    @Override
    public void run() {
    try {
    System.out.println(Thread.currentThread().getName() + " waiting for signal.");
    semaphore.acquire(); // 等待信号量
    System.out.println(Thread.currentThread().getName() + " is now running.");
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    }
    }
    }
  • CountDownLatch:适用于一组线程等待一个事件完成后再同时继续执行。初始值设置为1,所有子线程调用 await() 等待,等事件发生时调用 countDown() 方法计数减为0。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    // 用 CountDownLatch 初始化一个计数值,并让线程等待直到计数归零。
    // 各线程先启动并等待 latch.await()。
    // 主线程模拟事件准备,准备完成后 countDown(),所有线程被释放同时执行。
    import java.util.concurrent.CountDownLatch;

    public class CountDownLatchExample {
    public static void main(String[] args) throws InterruptedException {
    int threadCount = 5;
    CountDownLatch latch = new CountDownLatch(1); // 初始化事件完成计数器

    for (int i = 0; i < threadCount; i++) {
    new Thread(new Task(latch)).start();
    }

    System.out.println("Main thread doing setup work...");
    Thread.sleep(2000); // 模拟某个事件准备时间

    System.out.println("Main thread completed setup, releasing all threads.");
    latch.countDown(); // 初始化完成,所有等待线程可继续
    }
    }

    class Task implements Runnable {
    private final CountDownLatch latch;

    public Task(CountDownLatch latch) {
    this.latch = latch;
    }

    @Override
    public void run() {
    try {
    System.out.println(Thread.currentThread().getName() + " is waiting for setup.");
    latch.await(); // 等待事件完成
    System.out.println(Thread.currentThread().getName() + " is now running.");
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    }
    }
    }
  • CyclicBarrier:可以使一组线程到达屏障后,再一同执行。特别适合周期性任务,在每个循环的关键点上使得所有线程保持同步。适用于一组线程都到达某个“屏障”点时,再一同继续执行。可以多次重用,适合需要反复等待的场景。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    import java.util.concurrent.BrokenBarrierException;
    import java.util.concurrent.CyclicBarrier;

    // 每个线程到达屏障后调用 await(),等到所有线程都到达时触发屏障,释放所有线程。
    // barrier.await() 的重置特性使 CyclicBarrier 可重复使用,适合周期性同步。
    public class CyclicBarrierExample {
    public static void main(String[] args) {
    int threadCount = 5;
    CyclicBarrier barrier = new CyclicBarrier(threadCount, () -> {
    System.out.println("All threads reached the barrier, releasing them...");
    });

    for (int i = 0; i < threadCount; i++) {
    new Thread(new Task(barrier)).start();
    }
    }
    }

    class Task implements Runnable {
    private final CyclicBarrier barrier;

    public Task(CyclicBarrier barrier) {
    this.barrier = barrier;
    }

    @Override
    public void run() {
    try {
    System.out.println(Thread.currentThread().getName() + " is waiting at the barrier.");
    barrier.await(); // 等待其他线程
    System.out.println(Thread.currentThread().getName() + " is now running after the barrier.");
    } catch (InterruptedException | BrokenBarrierException e) {
    Thread.currentThread().interrupt();
    }
    }
    }

问:如何实现控制线程在某段时间内完成,不完成就撤销?⭐⭐

通过 FutureExecutorService 来控制线程在指定时间内完成任务,如果超时未完成则尝试撤销该线程。Future 提供了超时控制功能,允许在一定时间内等待线程完成。如果任务超时,可以调用 Future.cancel(true) 取消任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.util.concurrent.*;

public class TimeoutTaskExample {
public static void main(String[] args) {
// 创建线程池
ExecutorService executorService = Executors.newSingleThreadExecutor();

// 提交任务并获取 Future 对象
Future<String> future = executorService.submit(() -> {
try {
Thread.sleep(3000); // 模拟长时间运行任务
return "Task Completed";
} catch (InterruptedException e) {
return "Task Interrupted";
}
});

try {
// 尝试在指定时间内完成任务
String result = future.get(2, TimeUnit.SECONDS); // 2秒内获取任务结果
System.out.println(result);
} catch (TimeoutException e) {
System.out.println("Task timed out, attempting to cancel...");
future.cancel(true); // 超时未完成,尝试取消任务
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} finally {
// 关闭线程池,避免资源泄漏。
executorService.shutdown();
}
}
}

问:线程回调?

  • 线程回调用于在一个线程完成任务后通知另一个线程执行相应的操作。回调机制在异步编程中非常常见,通过在主线程中设置一个“回调方法”,异步线程完成任务后即触发此回调,告知主线程任务结果或执行后续操作。

  • 使用 Future 模拟回调机制,在 Future 中可以轮询 isDone(),定期检查任务完成状态,以达到回调的效果。不过这样实现并不算是真正的异步回调。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    import java.util.concurrent.*;

    public class FutureCallbackExample {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
    ExecutorService executor = Executors.newSingleThreadExecutor();

    // 提交任务
    Future<String> future = executor.submit(() -> {
    Thread.sleep(2000);
    return "Task Completed";
    });

    // 模拟回调
    while (!future.isDone()) {
    System.out.println("等待任务完成...");
    Thread.sleep(500); // 等待任务完成的过程中可以做其他事情
    }

    // 一旦任务完成,获取结果
    System.out.println("任务完成: " + future.get());

    executor.shutdown();
    }
    }
  • 使用 CompletableFuture 实现真正的异步回调:CompletableFuture 提供了更为直观的异步编程和回调支持,不需要手动轮询或阻塞等待。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    import java.util.concurrent.CompletableFuture;

    public class CompletableFutureCallbackExample {
    public static void main(String[] args) {
    // 提交异步任务,开启一个异步任务,返回 CompletableFuture。
    CompletableFuture.supplyAsync(() -> {
    try {
    Thread.sleep(2000); // 模拟耗时操作
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    return "Task Completed";
    }).thenAccept(result -> {
    // 任务完成后自动调用此回调方法并传递任务结果,主线程无需等待异步任务完成即可继续执行。
    // 回调操作,在任务完成后执行
    System.out.println("任务完成: " + result);
    });

    System.out.println("主线程可以继续执行其他任务...");

    // 为了观察效果,主线程需要等子线程任务完成(测试环境下)
    try {
    Thread.sleep(3000); // 等待异步任务执行完
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }

问:RPC 框架?设计一个 RPC,怎么实现?TODO

  • 什么是RPC框架?

    • RPC(Remote Procedure Call,远程过程调用)框架用于在不同进程、不同机器间调用服务,像是在本地直接调用函数。通过序列化、传输协议、网络通信等机制,RPC实现跨进程的透明通信,屏蔽了底层网络的细节,调用端可以像调用本地服务一样调用远程方法。
  • RPC调用流程:

    • 客户端:如 Dubbo 是基于接口的远程方法调用,Java中接口不能直接调用实例方法,必须通过其实现类对象来完成此操作,意味着客户端必须为这些接口生成代理对象,对此 Java 提供了 ProxyInvocationHandler 生成动态代理的支持;代理对象调用指定方法时实际会执行InvocationHandler 中定义的 #invoke 方法,在该方法中完成远程方法调用并获取结果。

    • 注册中心:若集群的节点数量很大的话,管理服务地址也是一件繁琐的事情,常见的做法是各个服务节点将自己的地址和提供的服务列表注册到一个注册中心,由注册中心来统一管理服务列表;这样的做法解决问题的同时为客户端增加了一项新的工作—服务发现,通俗来说就是从注册中心中找到远程方法对应的服务列表并通过某种策略从中选取一个服务地址来完成网络通信。

    • 服务端:负责提供服务接口的真正实现并在某个端口上监听网络请求,监听到请求后从网络请求中获取到对应的参数(比如服务接口、方法、请求参数等),再根据这些参数通过反射的方式调用接口的真正实现获取结果并将其写入对应的响应流中。

    流程:

    1. Client注册客户端信息,Server注册服务接口。
    2. Client初始化,从注册中心获取服务列表。
    3. 注册中心进行服务端上下线通知。
    4. Client负载引擎选择服务节点。
    5. Client封装请求为一个任务丢人线程池。
    6. Client从缓存中获取连接(没有则创建并放入缓存中)
    7. Client序列化编码,发送网络请求;Server反序列化解码。
    8. Server从缓存中获取处理类,接口限流、反射调用获取结果。
    9. Client响应请求结果。

    2

  • 设计RPC步骤:

    1. 定义通信协议:定义调用的请求格式、传输协议等。
    2. 接口定义和服务实现:定义服务接口(客户端和服务端共用),并在服务端实现该接口。
    3. 序列化与反序列化:将请求参数、返回值序列化为可传输的数据格式(如 JSON、ProtoBuf 等)。
    4. 网络通信:客户端通过网络(如 Socket、HTTP)发送请求,服务端接收并处理请求后返回结果。
    5. 动态代理生成 Stub:客户端通过动态代理生成的 Stub,使远程调用透明化。
    6. 负载均衡与容错处理(可选):处理多个服务实例的选择和故障情况。
  • 实现:

    • 服务端(生产者)

      • 服务接口:在 RPC 中,生产者和消费者有一个共同的服务接口 API。如下,定义一个 HelloService 接口。

        1
        2
        3
        4
        5
        6
        /**
        * @Descrption 服务接口
        ***/
        public interface HelloService {
        String sayHello(String somebody);
        }
      • 服务实现:生产者要提供服务接口的实现,创建 HelloServiceImpl 实现类。

        1
        2
        3
        4
        5
        6
        7
        8
        9
        /**
        * @Descrption 服务实现
        ***/
        public class HelloServiceImpl implements HelloService {
        @Override
        public String sayHello(String somebody) {
        return "hello " + somebody + "!";
        }
        }
      • 服务注册:本例使用 Spring 来管理 bean,采用自定义 XML 和解析器的方式来将服务实现类载入容器(当然也可以采用自定义注解的方式,此处不过多论述)并将服务接口信息注册到注册中心。

        首先自定义 XSD:分别指定 Schema 和 XSD,Schema 和对应 Handler 的映射。

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        <xsd:element name="service">
        <xsd:complexType>
        <xsd:complexContent>
        <xsd:extension base="beans:identifiedType">
        <xsd:attribute name="interface" type="xsd:string" use="required"/>
        <xsd:attribute name="timeout" type="xsd:int" use="required"/>
        <xsd:attribute name="serverPort" type="xsd:int" use="required"/>
        <xsd:attribute name="ref" type="xsd:string" use="required"/>
        <xsd:attribute name="weight" type="xsd:int" use="optional"/>
        <xsd:attribute name="workerThreads" type="xsd:int" use="optional"/>
        <xsd:attribute name="appKey" type="xsd:string" use="required"/>
        <xsd:attribute name="groupName" type="xsd:string" use="optional"/>
        </xsd:extension>
        </xsd:complexContent>
        </xsd:complexType>
        </xsd:element>

        Schema:

        1
        2
        http\://www.storm.com/schema/storm-service.xsd=META-INF/storm-service.xsd
        http\://www.storm.com/schema/storm-reference.xsd=META-INF/storm-reference.xsd

        Handler:

        1
        2
        http\://www.storm.com/schema/storm-service=com.hsunfkqm.storm.framework.spring.StormServiceNamespaceHandler
        http\://www.storm.com/schema/storm-reference=com.hsunfkqm.storm.framework.spring.StormRemoteReferenceNamespaceHandler

        将编写好的文件放入 Classpath 下的 META-INF 目录下:spring.handlers,spring.schemas,storm-service.xsd,storm-reference.xsd。

        在 Spring 配置文件中配置服务类:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        <!-- 发布远程服务 -->
        <bean id="helloService" class="com.hsunfkqm.storm.framework.test.HelloServiceImpl"/>
        <storm:service id="helloServiceRegister"
        interface="com.hsunfkqm.storm.framework.test.HelloService"
        ref="helloService"
        groupName="default"
        weight="2"
        appKey="ares"
        workerThreads="100"
        serverPort="8081"
        timeout="600"/>

        编写对应的 Handler 和 Parser:

        StormServiceNamespaceHandler:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        import org.springframework.beans.factory.xml.NamespaceHandlerSupport;

        /**
        * 服务发布自定义标签
        ***/
        public class StormServiceNamespaceHandler extends NamespaceHandlerSupport {
        @Override
        public void init() {
        registerBeanDefinitionParser("service", new ProviderFactoryBeanDefinitionParser());
        }
        }

        ProviderFactoryBeanDefinitionParser:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        protected Class getBeanClass(Element element) {
        return ProviderFactoryBean.class;
        }

        protected void doParse(Element element, BeanDefinitionBuilder bean) {

        try {
        String serviceItf = element.getAttribute("interface");
        String serverPort = element.getAttribute("serverPort");
        String ref = element.getAttribute("ref");
        // ....
        bean.addPropertyValue("serverPort", Integer.parseInt(serverPort));
        bean.addPropertyValue("serviceItf", Class.forName(serviceItf));
        bean.addPropertyReference("serviceObject", ref);
        //...
        if (NumberUtils.isNumber(weight)) {
        bean.addPropertyValue("weight", Integer.parseInt(weight));
        }
        //...
        } catch (Exception e) {
        // ...
        }
        }

        ProviderFactoryBean:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        49
        50
        51
        52
        53
        54
        55
        56
        57
        58
        59
        60
        61
        62
        63
        64
        65
        66
        67
        68
        69
        70
        71
        72
        73
        74
        75
        76
        77
        78
        79
        80
        81
        82
        83
        84
        85
        86
        87
        88
        89
        90
        91
        92
        93
        94
        95
        96
        97
        98
        99
        100
        101
        102
        103
        104
        105
        106
        107
        108
        109
        110
        111
        112
        113
        114
        115
        116
        117
        118
        119
        120
        /**
        * @Descrption 服务发布
        ***/
        public class ProviderFactoryBean implements FactoryBean, InitializingBean {

        //服务接口
        private Class<?> serviceItf;
        //服务实现
        private Object serviceObject;
        //服务端口
        private String serverPort;
        //服务超时时间
        private long timeout;
        //服务代理对象,暂时没有用到
        private Object serviceProxyObject;
        //服务提供者唯一标识
        private String appKey;
        //服务分组组名
        private String groupName = "default";
        //服务提供者权重,默认为 1 , 范围为 [1-100]
        private int weight = 1;
        //服务端线程数,默认 10 个线程
        private int workerThreads = 10;

        @Override
        public Object getObject() throws Exception {
        return serviceProxyObject;
        }

        @Override
        public Class<?> getObjectType() {
        return serviceItf;
        }

        @Override
        public void afterPropertiesSet() throws Exception {
        //启动 Netty 服务端
        NettyServer.singleton().start(Integer.parseInt(serverPort));
        //注册到 zk, 元数据注册中心
        List<ProviderService> providerServiceList = buildProviderServiceInfos();
        IRegisterCenter4Provider registerCenter4Provider = RegisterCenter.singleton();
        registerCenter4Provider.registerProvider(providerServiceList);
        }
        }

        //================RegisterCenter#registerProvider======================
        @Override
        public void registerProvider(final List<ProviderService> serviceMetaData) {
        if (CollectionUtils.isEmpty(serviceMetaData)) {
        return;
        }

        //连接 zk, 注册服务
        synchronized (RegisterCenter.class) {
        for (ProviderService provider : serviceMetaData) {
        String serviceItfKey = provider.getServiceItf().getName();

        List<ProviderService> providers = providerServiceMap.get(serviceItfKey);
        if (providers == null) {
        providers = Lists.newArrayList();
        }
        providers.add(provider);
        providerServiceMap.put(serviceItfKey, providers);
        }

        if (zkClient == null) {
        zkClient = new ZkClient(ZK_SERVICE, ZK_SESSION_TIME_OUT, ZK_CONNECTION_TIME_OUT, new SerializableSerializer());
        }

        //创建 ZK 命名空间/当前部署应用 APP 命名空间/
        String APP_KEY = serviceMetaData.get(0).getAppKey();
        String ZK_PATH = ROOT_PATH + "/" + APP_KEY;
        boolean exist = zkClient.exists(ZK_PATH);
        if (!exist) {
        zkClient.createPersistent(ZK_PATH, true);
        }

        for (Map.Entry<String, List<ProviderService>> entry : providerServiceMap.entrySet()) {
        //服务分组
        String groupName = entry.getValue().get(0).getGroupName();
        //创建服务提供者
        String serviceNode = entry.getKey();
        String servicePath = ZK_PATH + "/" + groupName + "/" + serviceNode + "/" + PROVIDER_TYPE;
        exist = zkClient.exists(servicePath);
        if (!exist) {
        zkClient.createPersistent(servicePath, true);
        }

        //创建当前服务器节点
        int serverPort = entry.getValue().get(0).getServerPort();//服务端口
        int weight = entry.getValue().get(0).getWeight();//服务权重
        int workerThreads = entry.getValue().get(0).getWorkerThreads();//服务工作线程
        String localIp = IPHelper.localIp();
        String currentServiceIpNode = servicePath + "/" + localIp + "|" + serverPort + "|" + weight + "|" + workerThreads + "|" + groupName;
        exist = zkClient.exists(currentServiceIpNode);
        if (!exist) {
        //注意,这里创建的是临时节点
        zkClient.createEphemeral(currentServiceIpNode);
        }
        //监听注册服务的变化,同时更新数据到本地缓存
        zkClient.subscribeChildChanges(servicePath, new IZkChildListener() {
        @Override
        public void handleChildChange(String parentPath, List<String> currentChilds) throws Exception {
        if (currentChilds == null) {
        currentChilds = Lists.newArrayList();
        }
        //存活的服务 IP 列表
        List<String> activityServiceIpList = Lists.newArrayList(Lists.transform(currentChilds, new Function<String, String>() {
        @Override
        public String apply(String input) {
        return StringUtils.split(input, "|")[0];
        }
        }));
        refreshActivityService(activityServiceIpList);
        }
        });

        }
        }
        }

        至此服务实现类已被载入 Spring 容器中,且服务接口信息也注册到了注册中心。

      • 网络通信:作为生产者对外提供 RPC 服务,必须有一个网络程序来来监听请求和做出响应。在 Java 领域 Netty 是一款高性能的 NIO 通信框架,很多的框架的通信都是采用 Netty 来实现的,本例中也采用它当做通信服务器。

        构建并启动 Netty 服务监听指定端口:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        public void start(final int port) {
        synchronized (NettyServer.class) {
        if (bossGroup != null || workerGroup != null) {
        return;
        }

        bossGroup = new NioEventLoopGroup();
        workerGroup = new NioEventLoopGroup();
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap
        .group(bossGroup, workerGroup)
        .channel(NioServerSocketChannel.class)
        .option(ChannelOption.SO_BACKLOG, 1024)
        .childOption(ChannelOption.SO_KEEPALIVE, true)
        .childOption(ChannelOption.TCP_NODELAY, true)
        .handler(new LoggingHandler(LogLevel.INFO))
        .childHandler(new ChannelInitializer<SocketChannel>() {
        @Override
        protected void initChannel(SocketChannel ch) throws Exception {
        //注册解码器 NettyDecoderHandler
        ch.pipeline().addLast(new NettyDecoderHandler(StormRequest.class, serializeType));
        //注册编码器 NettyEncoderHandler
        ch.pipeline().addLast(new NettyEncoderHandler(serializeType));
        //注册服务端业务逻辑处理器 NettyServerInvokeHandler
        ch.pipeline().addLast(new NettyServerInvokeHandler());
        }
        });
        try {
        channel = serverBootstrap.bind(port).sync().channel();
        } catch (InterruptedException e) {
        throw new RuntimeException(e);
        }
        }
        }

        上面的代码中向 Netty 服务的 Pipeline 中添加了编解码和业务处理器,当接收到请求时,经过编解码后,真正处理业务的是业务处理器,即 NettyServerInvokeHandler,该处理器继承自 SimpleChannelInboundHandler,当数据读取完成将触发一个事件,并调用 NettyServerInvokeHandler#channelRead0 方法来处理请求。

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        49
        50
        51
        52
        53
        54
        55
        56
        57
        58
        59
        60
        61
        62
        63
        64
        65
        66
        @Override
        protected void channelRead0(ChannelHandlerContext ctx, StormRequest request) throws Exception {
        if (ctx.channel().isWritable()) {
        //从服务调用对象里获取服务提供者信息
        ProviderService metaDataModel = request.getProviderService();
        long consumeTimeOut = request.getInvokeTimeout();
        final String methodName = request.getInvokedMethodName();

        //根据方法名称定位到具体某一个服务提供者
        String serviceKey = metaDataModel.getServiceItf().getName();
        //获取限流工具类
        int workerThread = metaDataModel.getWorkerThreads();
        Semaphore semaphore = serviceKeySemaphoreMap.get(serviceKey);
        if (semaphore == null) {
        synchronized (serviceKeySemaphoreMap) {
        semaphore = serviceKeySemaphoreMap.get(serviceKey);
        if (semaphore == null) {
        semaphore = new Semaphore(workerThread);
        serviceKeySemaphoreMap.put(serviceKey, semaphore);
        }
        }
        }

        //获取注册中心服务
        IRegisterCenter4Provider registerCenter4Provider = RegisterCenter.singleton();
        List<ProviderService> localProviderCaches = registerCenter4Provider.getProviderServiceMap().get(serviceKey);

        Object result = null;
        boolean acquire = false;

        try {
        ProviderService localProviderCache = Collections2.filter(localProviderCaches, new Predicate<ProviderService>() {
        @Override
        public boolean apply(ProviderService input) {
        return StringUtils.equals(input.getServiceMethod().getName(), methodName);
        }
        }).iterator().next();
        Object serviceObject = localProviderCache.getServiceObject();

        //利用反射发起服务调用
        Method method = localProviderCache.getServiceMethod();
        //利用 semaphore 实现限流
        acquire = semaphore.tryAcquire(consumeTimeOut, TimeUnit.MILLISECONDS);
        if (acquire) {
        result = method.invoke(serviceObject, request.getArgs());
        //System.out.println("---------------"+result);
        }
        } catch (Exception e) {
        System.out.println(JSON.toJSONString(localProviderCaches) + " " + methodName+" "+e.getMessage());
        result = e;
        } finally {
        if (acquire) {
        semaphore.release();
        }
        }
        //根据服务调用结果组装调用返回对象
        StormResponse response = new StormResponse();
        response.setInvokeTimeout(consumeTimeOut);
        response.setUniqueKey(request.getUniqueKey());
        response.setResult(result);
        //将服务调用返回对象回写到消费端
        ctx.writeAndFlush(response);
        } else {
        logger.error("------------channel closed!---------------");
        }
        }

        此处还有部分细节如自定义的编解码器等,篇幅所限不在此详述,继承 MessageToByteEncoder 和 ByteToMessageDecoder 覆写对应的 encode 和 decode 方法即可自定义编解码器,使用到的序列化工具如 Hessian/Proto 等可参考对应的官方文档。

        请求和响应包装:

        为便于封装请求和响应,定义两个 bean 来表示请求和响应。

        请求:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        /**
        ***/
        public class StormRequest implements Serializable {

        private static final long serialVersionUID = -5196465012408804755L;
        //UUID,唯一标识一次返回值
        private String uniqueKey;
        //服务提供者信息
        private ProviderService providerService;
        //调用的方法名称
        private String invokedMethodName;
        //传递参数
        private Object[] args;
        //消费端应用名
        private String appName;
        //消费请求超时时长
        private long invokeTimeout;
        // getter/setter
        }

        响应:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        /**
        ***/
        public class StormResponse implements Serializable {
        private static final long serialVersionUID = 5785265307118147202L;
        //UUID, 唯一标识一次返回值
        private String uniqueKey;
        //客户端指定的服务超时时间
        private long invokeTimeout;
        //接口调用返回的结果对象
        private Object result;
        //getter/setter
        }
    • 客户端(消费者)

      客户端(消费者)在 RPC 调用中主要是生成服务接口的代理对象,并从注册中心获取对应的服务列表发起网络请求。

      客户端和服务端一样采用 Spring 来管理 bean 解析 XML 配置等不再赘述,重点看下以下几点:

      1. 通过 JDK 动态代理来生成引入服务接口的代理对象:

        1
        2
        3
        public Object getProxy() {
        return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class<?>[]{targetInterface}, this);
        }
      2. 从注册中心获取服务列表并依据某种策略选取其中一个服务节点

        1
        2
        3
        4
        5
        6
        7
        8
        //服务接口名称
        String serviceKey = targetInterface.getName();
        //获取某个接口的服务提供者列表
        IRegisterCenter4Invoker registerCenter4Consumer = RegisterCenter.singleton();
        List<ProviderService> providerServices = registerCenter4Consumer.getServiceMetaDataMap4Consume().get(serviceKey);
        //根据软负载策略,从服务提供者列表选取本次调用的服务提供者
        ClusterStrategy clusterStrategyService = ClusterEngine.queryClusterStrategy(clusterStrategy);
        ProviderService providerService = clusterStrategyService.select(providerServices);
      3. 通过 Netty 建立连接,发起网络请求。Netty 的响应是异步的,为了在方法调用返回前获取到响应结果,需要将异步的结果同步化。

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        49
        50
        51
        52
        53
        54
        55
        56
        57
        58
        59
        60
        61
        62
        63
        /**
        * 欢迎关注公众号:java后端技术全栈
        ***/
        public class RevokerProxyBeanFactory implements InvocationHandler {
        private ExecutorService fixedThreadPool = null;
        //服务接口
        private Class<?> targetInterface;
        //超时时间
        private int consumeTimeout;
        //调用者线程数
        private static int threadWorkerNumber = 10;
        //负载均衡策略
        private String clusterStrategy;

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        ...

        //复制一份服务提供者信息
        ProviderService newProvider = providerService.copy();
        //设置本次调用服务的方法以及接口
        newProvider.setServiceMethod(method);
        newProvider.setServiceItf(targetInterface);

        //声明调用 AresRequest 对象,AresRequest 表示发起一次调用所包含的信息
        final StormRequest request = new StormRequest();
        //设置本次调用的唯一标识
        request.setUniqueKey(UUID.randomUUID().toString() + "-" + Thread.currentThread().getId());
        //设置本次调用的服务提供者信息
        request.setProviderService(newProvider);
        //设置本次调用的方法名称
        request.setInvokedMethodName(method.getName());
        //设置本次调用的方法参数信息
        request.setArgs(args);

        try {
        //构建用来发起调用的线程池
        if (fixedThreadPool == null) {
        synchronized (RevokerProxyBeanFactory.class) {
        if (null == fixedThreadPool) {
        fixedThreadPool = Executors.newFixedThreadPool(threadWorkerNumber);
        }
        }
        }
        //根据服务提供者的 ip,port, 构建 InetSocketAddress 对象,标识服务提供者地址
        String serverIp = request.getProviderService().getServerIp();
        int serverPort = request.getProviderService().getServerPort();
        InetSocketAddress inetSocketAddress = new InetSocketAddress(serverIp, serverPort);
        //提交本次调用信息到线程池 fixedThreadPool, 发起调用
        Future<StormResponse> responseFuture = fixedThreadPool.submit(RevokerServiceCallable.of(inetSocketAddress, request));
        //获取调用的返回结果
        StormResponse response = responseFuture.get(request.getInvokeTimeout(), TimeUnit.MILLISECONDS);
        if (response != null) {
        return response.getResult();
        }
        } catch (Exception e) {
        throw new RuntimeException(e);
        }
        return null;
        }
        // ...
        }
      4. Netty 异步返回的结果存入阻塞队列

        1
        2
        3
        4
        5
        @Override
        protected void channelRead0(ChannelHandlerContext channelHandlerContext, StormResponse response) throws Exception {
        //将 Netty 异步返回的结果存入阻塞队列,以便调用端同步获取
        RevokerResponseHolder.putResultValue(response);
        }
      5. 请求发出后同步获取结果

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        //提交本次调用信息到线程池 fixedThreadPool, 发起调用
        Future<StormResponse> responseFuture = fixedThreadPool.submit(RevokerServiceCallable.of(inetSocketAddress, request));
        //获取调用的返回结果
        StormResponse response = responseFuture.get(request.getInvokeTimeout(), TimeUnit.MILLISECONDS);
        if (response != null) {
        return response.getResult();
        }

        //===================================================
        //从返回结果容器中获取返回结果,同时设置等待超时时间为 invokeTimeout
        long invokeTimeout = request.getInvokeTimeout();
        StormResponse response = RevokerResponseHolder.getValue(request.getUniqueKey(), invokeTimeout);
    • 测试

      Server:

      1
      2
      3
      4
      5
      6
      7
      public class MainServer {
      public static void main(String[] args) throws Exception {
      //发布服务
      final ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("storm-server.xml");
      System.out.println(" 服务发布完成");
      }
      }

      Client:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      public class Client {

      private static final Logger logger = LoggerFactory.getLogger(Client.class);

      public static void main(String[] args) throws Exception {

      final ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("storm-client.xml");
      final HelloService helloService = (HelloService) context.getBean("helloService");
      String result = helloService.sayHello("World");
      System.out.println(result);
      for (;;) {

      }
      }
      }

https://itmtx.cn/column/11

https://github.com/Veal98/RPC-FromScratch

问:线程间的通信可以怎么做?线程间相互通知你会怎么做,代码要怎么实现?⭐⭐

线程间的通信可以怎么做?

  1. 使用synchronized + wait / notify / notifyAll 实现线程间通信,调用这些方法前线程必须获得对象锁。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    class SharedData {
    private int data;
    private boolean available = false;

    public synchronized void produce(int value) throws InterruptedException {
    while (available) { // 如果已有数据,等待消费者消费
    wait();
    }
    data = value;
    available = true;
    System.out.println("Produced: " + data);
    notifyAll(); // 通知消费者
    }

    public synchronized void consume() throws InterruptedException {
    while (!available) { // 如果无数据,等待生产者生产
    wait();
    }
    System.out.println("Consumed: " + data);
    available = false;
    notifyAll(); // 通知生产者
    }
    }

    public class WaitNotifyExample {
    public static void main(String[] args) {
    SharedData sharedData = new SharedData();

    Thread producer = new Thread(() -> {
    try {
    for (int i = 0; i < 5; i++) {
    sharedData.produce(i);
    }
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    }
    });

    Thread consumer = new Thread(() -> {
    try {
    for (int i = 0; i < 5; i++) {
    sharedData.consume();
    }
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    }
    });

    producer.start();
    consumer.start();
    }
    }
  2. 使用 LockCondition 实现线程间通信:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;

    class SharedResource {
    private int data;
    private boolean available = false;
    private final Lock lock = new ReentrantLock();
    private final Condition produced = lock.newCondition();
    private final Condition consumed = lock.newCondition();

    public void produce(int value) throws InterruptedException {
    lock.lock();
    try {
    while (available) {
    produced.await(); // 等待消费者消费数据
    }
    data = value;
    available = true;
    System.out.println("Produced: " + data);
    consumed.signal(); // 通知消费者可以消费了
    } finally {
    lock.unlock();
    }
    }

    public void consume() throws InterruptedException {
    lock.lock();
    try {
    while (!available) {
    consumed.await(); // 等待生产者生产数据
    }
    System.out.println("Consumed: " + data);
    available = false;
    produced.signal(); // 通知生产者可以生产了
    } finally {
    lock.unlock();
    }
    }
    }

    public class LockConditionExample {
    public static void main(String[] args) {
    SharedResource sharedResource = new SharedResource();

    Thread producer = new Thread(() -> {
    try {
    for (int i = 0; i < 5; i++) {
    sharedResource.produce(i);
    }
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    }
    });

    Thread consumer = new Thread(() -> {
    try {
    for (int i = 0; i < 5; i++) {
    sharedResource.consume();
    }
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    }
    });

    producer.start();
    consumer.start();
    }
    }
  3. 使用 BlockingQueue 实现线程间通信:是线程安全的队列,可以用于生产者-消费者模式。生产者可以往队列中放数据,消费者从队列中取数据,自动阻塞和唤醒,无需显式的同步代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    class BlockingQueueExample {
    public static void main(String[] args) {
    BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(1);

    Thread producer = new Thread(() -> {
    try {
    for (int i = 0; i < 5; i++) {
    queue.put(i);
    System.out.println("Produced: " + i);
    }
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    }
    });

    Thread consumer = new Thread(() -> {
    try {
    for (int i = 0; i < 5; i++) {
    int data = queue.take();
    System.out.println("Consumed: " + data);
    }
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    }
    });

    producer.start();
    consumer.start();
    }
    }

4. Semaphore

信号量控制资源访问数量,可以实现限流或同步控制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
javaCopyEditimport java.util.concurrent.Semaphore;

public class SemaphoreExample {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(1); // 允许一个线程访问

Runnable task = () -> {
try {
semaphore.acquire(); // 获取许可
System.out.println(Thread.currentThread().getName() + " 执行任务");
Thread.sleep(2000); // 模拟任务耗时
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 释放许可
}
};

new Thread(task).start();
new Thread(task).start();
}
}

5. CountDownLatch / CyclicBarrier

  • CountDownLatch:等待多个线程完成某个任务
  • CyclicBarrier:让多个线程在某个点上等待
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
javaCopyEditimport java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);

Runnable task = () -> {
System.out.println(Thread.currentThread().getName() + " 完成任务");
latch.countDown(); // 计数器减一
};

new Thread(task).start();
new Thread(task).start();
new Thread(task).start();

latch.await(); // 等待计数器变为 0
System.out.println("所有任务完成");
}
}

6. volatile 关键字

保证变量的可见性,适用于轻量级通信。

7. Future / CompletableFuture

适用于异步任务,获取结果或处理异常。

问:notify()与notifyAll()的区别?

  • **notify()**:该方法随机唤醒在对象监视器上等待的单个线程。如果有多个线程在等待,具体唤醒哪个线程是不可预测的,这通常由 JVM 的线程调度策略决定。
  • **notifyAll()**:该方法唤醒在对象监视器上等待的所有线程。这些线程会被唤醒并进入可运行状态,但并不意味着它们会立即开始执行。它们会等待重新获得对象的锁。

当调用 notifyAll() 时,所有等待该对象监视器的线程会被唤醒,并进入可运行状态。但是,这些线程仍需竞争对象的锁,只有获取到锁的线程才能继续执行。具体步骤如下:

  1. 唤醒所有线程:调用 notifyAll() 后,所有等待该对象监视器的线程会被唤醒。
  2. 竞争锁:被唤醒的线程不会立刻执行,而是进入可运行状态。它们必须争夺对象的锁,只有获得锁的线程才能继续执行。
  3. 锁的排他性:由于 synchronized 方法或块的排他性,只有一个线程可以持有该对象的锁。其他线程即使被唤醒,必须等待该线程释放锁才能继续执行。

安全性考虑

  • 使用 notify() 的场景:如果你清楚在某一时刻只有一个消费者或者一个生产者可以处理数据,并且你只希望唤醒一个线程,那么 notify() 是合适的。
  • 使用 notifyAll() 的场景:如果你有多个消费者或生产者并且任何一个都可能适合处理共享资源,使用 notifyAll() 更为安全,尽管它会唤醒所有等待的线程。使用 notifyAll() 可以避免饥饿现象(即某些线程永远得不到执行机会),但可能会增加上下文切换的开销。

1.2 ThreadLocal

问:谈一下ThreadLocal?应用场景?原理?与 Thread 类的关系?⭐

  • 什么是ThreadLocal?

    • ThreadLocal 是 Java 中的一个类,用于创建线程局部变量。每个线程都可以通过 ThreadLocal 来创建和使用自己的变量副本,这些副本是线程隔离的,互不影响。这样可以方便地在多线程环境中存储线程特定的数据。
  • 应用场景:

    1. 用户会话管理:在 web 应用中,使用 ThreadLocal 存储用户的会话信息,使得每个线程在处理请求时能够访问该信息,而不会互相干扰。

    2. 数据库连接:可以用 ThreadLocal 存储数据库连接对象connection,确保每个线程使用自己的连接,避免竞争条件和连接共享。

    3. 事务管理:在一些框架中,例如 Spring,可以使用 ThreadLocal 来管理事务的上下文,使得事务在每个线程中独立处理。

    4. 单例模式:可以用 ThreadLocal 来实现线程安全的单例对象,在多线程环境下确保每个线程都使用自己的单例实例。

    5. 解决simpleDateFormat线程安全问题SimpleDateFormat 是非线程安全的,因此在多线程环境中使用时,多个线程同时访问同一个 SimpleDateFormat 实例会导致数据竞争和错误的结果。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      public class ThreadLocalDateFormatExample {
      // 使用 ThreadLocal 创建一个 SimpleDateFormat 实例
      private static final ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
      ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

      public static void main(String[] args) {
      Runnable task = () -> {
      // 每个线程使用自己的 SimpleDateFormat 实例
      SimpleDateFormat sdf = dateFormatThreadLocal.get();
      String dateStr = sdf.format(new Date());
      System.out.println("Thread: " + Thread.currentThread().getName() + " - Date: " + dateStr);
      };

      // 创建多个线程
      Thread thread1 = new Thread(task);
      Thread thread2 = new Thread(task);
      thread1.start();
      thread2.start();
      }
      }
    6. 可能出现内存泄漏

      • ThreadLocal 持有对其键的强引用(即 Thread 对象),如果 Thread 对象长时间存活而没有被回收,ThreadLocal 的值(如 SimpleDateFormat 实例)也将无法被回收,造成内存泄漏。
      • 线程池中的线程通常会被复用,而不是在完成任务后就被销毁。因此,即使某个线程不再需要 ThreadLocal 存储的值,该值仍然会存在于 ThreadLocalMap 中,直到线程池中的线程被回收(通常不会发生,因为线程池在应用生命周期内保持存活)。这意味着,若不手动调用 remove() 方法来清除不再需要的 ThreadLocal 值,可能会导致内存泄漏。不要与线程池配合,因为worker往往是不会退出的;
      • 所以建议不再需要 ThreadLocal 变量时,显式调用 remove() 方法,以清理 ThreadLocalMap 中的条目,避免内存泄漏。
  • 原理:每个线程都有一个独立的 ThreadLocalMap,对应成员变量threadLocals,key为本身,value为实际存值的变量副本。

    • 每个线程都有一个 ThreadLocalMap:当线程访问 ThreadLocalget()set() 方法时,会通过 Thread 类的 threadLocalMap 属性获取到当前线程的 ThreadLocalMap 实例。
    • 存储与检索ThreadLocalMapThreadLocal 对象作为键,线程的值作为值,这样每个线程都可以存取自己的值。
    • 垃圾回收:如果一个 ThreadLocal 对象不再被使用(没有强引用),那么它在 ThreadLocalMap 中的值会被保留,直到该线程结束。如果线程不再使用,可以通过 remove() 方法清除相应的值,以避免内存泄漏。
  • 与 Thread 类的关系:

    • ThreadLocal 不是 Thread 类的子类,但它是与 Thread 类密切相关的。每个 Thread 对象内部都有一个 ThreadLocalMap,这个 ThreadLocalMap 存储了该线程使用的 ThreadLocal 变量及其对应的值。
  • 案例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    public class ThreadLocalExample {
    // 创建一个 ThreadLocal 变量
    private static ThreadLocal<Integer> threadLocalValue = ThreadLocal.withInitial(() -> 0);

    public static void main(String[] args) {
    // 创建多个线程
    Thread thread1 = new Thread(() -> {
    for (int i = 0; i < 5; i++) {
    threadLocalValue.set(threadLocalValue.get() + 1);
    System.out.println("Thread 1: " + threadLocalValue.get());
    }
    });

    Thread thread2 = new Thread(() -> {
    for (int i = 0; i < 5; i++) {
    threadLocalValue.set(threadLocalValue.get() + 1);
    System.out.println("Thread 2: " + threadLocalValue.get());
    }
    });

    thread1.start();
    thread2.start();
    }
    }

问:static 能不能修饰 ThreadLocal?

  • 可以,且通常将ThreadLocal变量声明为私有静态。这样做有好处也有坏处,主要是为了避免重复的创建TSO(thread specific object,即与线程相关的变量),坏处是可能导致内存泄漏
  • 如果声明ThreadLocal为某个类的实例变量,每创建一个类的实例就会创建新的ThreadLocal实例,导致同一个线程可能访问到同一个TSO类的不同实例,也因为重复创建相同的对象导致浪费。
  • 因为类对ThreadLocal的静态引用,导致最终生成了对ThreadLocal的一条可达引用链路,使ThreadLocal实例不会被垃圾回收,即产生了内存泄漏。
  • 所以使用ThreadLocal需要手动回收这部分内存,可以remove或使ThreadLocal变量=null。

问:谈一下ThreadLocal的内存泄漏问题?ThreadLocal为什么会出现内存泄漏?⭐⭐⭐

  • 首先每个线程内部都有一个ThreadLocalMap实例。用于存储该线程中所有ThreadLocal的变量值。ThreadLocalMap键是 ThreadLocal 实例的弱引用,值是一个强引用
  • 如果某个 ThreadLocal 实例没有强引用指向它时,GC 会回收该 ThreadLocal。但是回收掉 ThreadLocal 的键之后,其对应的值(ThreadLocal 保存的数据)仍然存放在 ThreadLocalMap 中,形成 “键为 null,值仍存在” 的孤岛结构。
  • 普通线程结束时,其 ThreadLocalMap 会随着线程销毁而被回收。但如果线程是线程池中的线程,由于线程会被复用,ThreadLocalMap 的内容可能长期存在,导致无法回收未清理的值,从而引发内存泄漏。
  • 解决办法:
    1. 在任务执行完毕后,调用 ThreadLocal.remove() 方法显式清理数据。

问:如果我们要获取父线程的ThreadLocal值呢?

  • 在 Java 的标准 ThreadLocal 实现中,ThreadLocal 是线程隔离的,不同线程无法直接访问彼此的 ThreadLocal 值。但是我们可以用InteritableThreadLocal来实现这个功能。

  • InteritableThreadLocal继承自ThreadLocal,允许子线程从父线程继承 ThreadLocal 的值。重写了createdMap方法,已经对应的get和set方法,不是在利用了threadLocals,而是interitableThreadLocals变量。

  • InheritableThreadLocal 的默认行为是简单拷贝父线程的值到子线程。但有时你可能希望在拷贝时进行某些转换或自定义处理,可以通过重写 InheritableThreadLocal#childValue 方法实现。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    public class InheritableThreadLocalExample {
    private static final InheritableThreadLocal<String> inheritableThreadLocal =
    new InheritableThreadLocal<>();

    public static void main(String[] args) {
    // 设置父线程的 ThreadLocal 值
    inheritableThreadLocal.set("Parent Thread Value");

    Thread childThread = new Thread(() -> {
    // 子线程获取继承的值
    System.out.println("Child Thread: " + inheritableThreadLocal.get());
    });

    childThread.start();

    try {
    childThread.join();
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    }
    }
    }
    // 输出Child Thread: Parent Thread Value


    private static final InheritableThreadLocal<String> inheritableThreadLocal =
    new InheritableThreadLocal<>() {
    @Override
    protected String childValue(String parentValue) {
    // 自定义子线程继承值的逻辑
    return parentValue + " (Modified for Child)";
    }
    };
  • 线程池场景中,因为复用线程,如果线程的 InheritableThreadLocal 值没有被清理,线程可能会继续持有上一个任务的值,导致线程间数据污染。而且因为线程由线程池管理而不是调用者,所以没有明确的父子关系。

  • 在 Spring 框架中,InheritableThreadLocal 的功能可以通过 TransmittableThreadLocal(由阿里巴巴开源的组件 TTL)增强。这种方式特别适合线程池场景,解决了 InheritableThreadLocal 在线程池中因线程复用而导致父子线程值错乱的问题。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    import com.alibaba.ttl.TransmittableThreadLocal;

    public class TransmittableThreadLocalExample {
    private static final TransmittableThreadLocal<String> threadLocal =
    new TransmittableThreadLocal<>();

    public static void main(String[] args) {
    threadLocal.set("Parent Thread Value");

    Thread childThread = new Thread(() -> {
    System.out.println("Child Thread: " + threadLocal.get());
    });

    childThread.start();

    try {
    childThread.join();
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    }
    }
    }

1.3 Thread

问:Thread类里有哪些常用方法?

  1. start():启动线程,调用线程的 run() 方法。不能多次调用同一个线程对象的 start() 方法,否则会抛出 IllegalThreadStateException
  2. run():定义线程执行的具体逻辑。通常不直接调用,而是通过 start() 启动线程后由 JVM 调用。
  3. join():当前线程等待指定线程执行完毕。
  4. join(long millis):等待指定的毫秒时间。
  5. interrupt():中断线程,将线程的中断状态设置为 true。线程可以通过 Thread.interrupted()isInterrupted() 检测中断状态。如果线程正在执行阻塞操作(如 sleep()wait() 等),会抛出 InterruptedException
  6. isInterrupted():检测线程是否被中断。不会清除中断状态。
  7. interrupted():(静态方法)检测当前线程是否被中断,并清除中断状态。
  8. isAlive():检查线程是否仍处于活动状态(尚未终止)。
  9. setPriority(int priority):设置线程的优先级,取值范围为 Thread.MIN_PRIORITY (1) 到 Thread.MAX_PRIORITY (10),默认值是 Thread.NORM_PRIORITY (5)。getPriority() 获取线程的优先级。
  10. getName() 和 setName(String name):获取或设置线程的名称。
  11. getId():返回线程的唯一标识符。
  12. currentThread():(静态方法)获取当前正在执行的线程。
  13. getState():获取线程的当前状态,如 NEWRUNNABLEBLOCKEDWAITING 等。
  14. sleep(long millis):(静态方法)让当前线程进入休眠状态,暂停执行指定的毫秒时间。
  15. yield():(静态方法)让出 CPU 使用权,允许其他线程获取执行机会。具体行为依赖于操作系统的线程调度机制。
  16. setDaemon(boolean on):设置线程是否为守护线程,必须在线程启动之前调用。守护线程在所有用户线程结束时自动终止。
  17. isDaemon():检查线程是否为守护线程。

问:interrupt/isInterrupted/interrupt区别?

  • interrupt():调用该方法的线程的状态为将被置为”中断”状态(set操作)。如果目标线程正在执行阻塞方法(如 sleep()wait()join()),这些方法会抛出 InterruptedException,并清除中断状态。
  • isinterrupted():检测目标线程的中断状态(truefalse)。不会清除线程的中断状态。
  • interrupted():是静态方法:内部实现是调用的当前线程的isInterrupted(),并且会重置当前线程的中断状态(getandset)用于判断当前线程是否被中断,同时清除中断状态,避免后续误判。

问:谈一下start和run方法的区别?⭐

  • start() 方法:启动一个新线程,并使其进入 就绪状态。调用 start() 后,JVM 会为该线程分配资源,并自动调用 run() 方法,执行线程任务。每个线程对象的 start() 方法只能调用一次,重复调用会抛出 IllegalThreadStateExceptionstart() 是一个原生方法,由 JVM 调用底层操作系统接口,启动一个新的线程。
    • 是否创建新线程:是,创建一个新线程,并执行 run() 方法
    • 场景:在需要真正实现多线程的场景中使用,启动新的线程来并发执行任务。
  • run() 方法:定义线程的具体执行逻辑。当run方法执行结束,线程随即终止,调用run方法并不能用来启动线程。run() 是一个普通的方法,直接调用时,代码会在当前线程中执行,而不会创建新的线程。
    • 是否创建新线程:否,在当前线程中调用 run() 方法。
    • 场景:仅用于定义线程的执行逻辑,通常不会直接调用它,而是通过 start() 间接调用。

问:sleep 和 wait 有什么区别?⭐⭐

  • sleep:属于线程类;
    • 定义:Thread.sleep(long millis)Thread 类的静态方法,用于让当前线程暂停执行一段时间。
  • wait:属于object类;
    • 定义:Object.wait()Object 类的实例方法,线程调用该方法后会进入等待状态,并释放当前持有的对象锁。
特性 sleep wait
所属类 Thread Object
是否释放锁 不释放锁 释放锁
调用位置 可在任何位置调用 必须在同步块或同步方法中调用
恢复方式 时间到期后自动恢复 需要其他线程调用 notifynotifyAll
与锁的关系 与锁机制无关 必须依赖锁机制
用途 让线程休眠一段时间 线程间通信,协调线程执行

问:sleep 和 yeild 方法有什么区别?

特性 sleep yield
作用 让线程暂停执行指定时间 让线程让出 CPU 时间片
状态转换 进入 TIMED_WAITING 状态 进入 RUNNABLE 状态
是否阻塞 阻塞线程,直到时间结束或被中断 不阻塞线程
是否释放锁 不释放锁 不释放锁
恢复机制 时间到后自动恢复 由线程调度器重新分配 CPU 时间片
调度器依赖 不依赖调度器 强依赖调度器,效果不确定
中断处理 可通过 InterruptedException 捕获 不响应中断

1.4 线程池

问:项目的用户量多吗?有用过线程池吗?用到多线程的优势在哪?⭐

  • 以前的项目用户量不大,但有学习和练习过线程池。
  • 多线程意味着能同时执行多个任务,利用多核处理器,提高并发性能,加快程序执行速度,提高系统 的吞吐量和响应能力。
  • 线程池则是用来管理和限制系统中执行线程的数量(统一管理,避免资源耗尽),重复的利用已创建的线程,防止内存的浪费(降低资源消耗),减少创建和销毁线程的次数,(提高响应速度)。

线程池优先把任务放入队列,而不是创建更多线程,适合CPU密集型任务,线程过多反而导致上下文切换等影响效率。对于IO密集的场景,一些线程池会优先创建线程,让单位时间内执行更多的任务。

池化技术:用空间换时间,把创建耗时的资源统一管理起来。

典型场景:

  1. 快速响应用户请求
    • 背景: 处理实时用户请求,需尽快返回响应。
    • 方案: 使用线程池执行多个查询任务并聚合结果,提高响应速度。
  2. 批量任务处理
    • 背景: 定期处理大量离线任务,如日志分析。
    • 方案: 使用ScheduledThreadPoolExecutor定时调度批量任务,确保任务按时执行。
  3. 动态线程池方案:在复杂业务场景中,静态配置的线程池可能无法满足高负载变化的需求。通过动态调整线程池参数,可以提升系统的弹性与稳定性。方案包括:
    • 实时监控: 收集线程池运行数据。
    • 参数调优: 根据业务需求动态调整线程池大小。
    • 策略优化: 根据任务类型和优先级灵活调整拒绝策略。

问:有哪几种线程池?⭐⭐

线程池 特点 核心线程 最大线程 阻塞队列 拒绝策略 适用场景
newFixedThreadPool(固定线程池) 固定大小,多余的任务排队等待 nThreads nThreads LinkedBlockingQueue AbortPolicy 需要执行大量短期异步任务,且线程数量应受限的情况。适用于处理CPU密集型的任务,适用执行长期的任务
newCachedThreadPool(缓存线程池) 根据需要调整线程数量,线程闲置超过 60 秒会自动回收 0 Integer.MAX_VALUE SynchronousQueue AbortPolicy 执行大量生命周期短的异步任务,且对资源消耗不敏感。适用于并发执行大量短期的小任务
newSingleThreadExecutor(单线程线程池) 只有一个线程,所有任务按顺序执行 1 1 LinkedBlockingQueue AbortPolicy 需要顺序执行任务,确保线程安全时。适用于串行执行任务的场景,一个任务一个任务地执行
newScheduledThreadPool(调度线程池) 支持任务定时和周期性执行 corePoolSize Integer.MAX_VALUE DelayedWorkQueue AbortPolicy 周期性执行任务,定时调度任务。需要限制线程数量的场景
newWorkStealingPool(工作窃取线程池, Java 8+) 基于分治模型,使用 ForkJoinPool 来实现工作窃取 parallelism(默认等于 CPU 核心数) 无显式配置(由 ForkJoinPool 管理) 多个任务队列(每个线程一个双端队列) 自适应,使用 ForkJoinPool 内置策略 需要提高并行任务执行效率的大规模任务

问:线程池状态?⭐

线程池有5种状态:running,showdown,stop,Tidying,TERMINATED。

状态名 描述
RUNNING 运行状态,接受新任务,并处理等待队列中的任务。创建线程默认此状态。
SHUTDOWN showdown()触发,不接受新任务,但处理等待队列中堆积的任务。
STOP showdownnow()触发,不接受新任务,不处理等待队列,并中断正在执行的任务。
TIDYING 当线程池状态为showdown或者stop,所有任务都已终止,会变为tidying,线程数为 0,正在运行终止处理,会调用钩子函数terminated()。
TERMINATED terminated()执行完成,终止处理完成,线程池完全终止。

状态转换如图:

    +-----------+    shutdown()      +------------+
    |  RUNNING  |------------------> |  SHUTDOWN  |
    +-----------+                    +------------+
          |                               |
          | shutdownNow()                 |
          v                               v
    +-----------+  -> 中断任务  ->    +------------+
    |    STOP   |------------------> |  TIDYING   |
    +-----------+                    +------------+
          |                               |
          |       任务结束                 |
          v                               v
    +-----------+  -> 终止完成 ->      +-------------+
    | TERMINATED| <------------------ |  TIDYING    |
    +-----------+                     +-------------+
  1. RUNNING -> SHUTDOWN:调用 shutdown(),停止接受新任务,但会处理阻塞队列中的任务。
  2. RUNNING/SHUTDOWN -> STOP:调用 shutdownNow(),停止接受任务,清空任务队列,中断正在执行的任务。
  3. SHUTDOWN -> TIDYING:队列和活动线程都为空,进入终止阶段。
  4. STOP -> TIDYING:所有任务都完成,线程池被中断。
  5. TIDYING -> TERMINATED:线程池终止完成,terminated() 钩子方法被执行。

图3 线程池生命周期

ThreadPoolExecutor 源码中,线程池状态通过一个 int 类型的变量 ctl 来管理,其中:

  • 高 3 位表示线程池状态。
  • 低 29 位表示线程池中的线程数量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1;

// 状态值定义
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;

// 状态检查方法
private static int runStateOf(int c) { return c & ~CAPACITY; }
private static int workerCountOf(int c) { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }

观察状态变化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import java.util.concurrent.*;

public class ThreadPoolStateExample {
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, 4, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<>(2));

// 提交一些任务
for (int i = 0; i < 6; i++) {
final int taskId = i + 1;
executor.submit(() -> {
System.out.println("Task " + taskId + " is running by " + Thread.currentThread().getName());
try {
Thread.sleep(2000); // 模拟任务执行
} catch (InterruptedException e) {
System.out.println("Task " + taskId + " was interrupted.");
}
System.out.println("Task " + taskId + " completed.");
});
}

System.out.println("Current Pool State: " + getPoolState(executor));

// 调用 shutdown(),查看状态
executor.shutdown();
System.out.println("After shutdown() - State: " + getPoolState(executor));

// 等待线程池终止
executor.awaitTermination(10, TimeUnit.SECONDS);
System.out.println("After termination - State: " + getPoolState(executor));
}

private static String getPoolState(ThreadPoolExecutor executor) {
if (executor.isShutdown()) return "SHUTDOWN";
if (executor.isTerminating()) return "TIDYING";
if (executor.isTerminated()) return "TERMINATED";
return "RUNNING";
}
}

问:ThreadPoolExecutor 初始化参数?corepoolSize 怎么设置,maxpoolsize 怎么设置,keep-alive 各种的?⭐⭐⭐

ThreadPoolExecutor 初始化参数?

  • corePoolSize(线程池基本大小):核心线程数,线程池中始终保持的最小线程数量,即使没有任务。
  • maximumPoolSize(线程池最大数量):若队列满了,会继续创建新的线程,直到达到线程池最大数量,当然若任务队列没有界限此参数就失效了。
  • ThreadFactory(线程工厂):用于设置创建线程的工厂,一般用于给线程统一命名。
  • keepAliveTime(线程活动保持时间):工作线程空闲后能够存活的时间,当任务很多且执行时间较短时,可以调高此值,提高线程的利用率。
  • TimeUnit(时间单位):可选的单位有天(DAYS)、小时(HOURS)、分钟 (MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和纳秒(NANOSECONDS,千分之一微秒)。
  • runnableTaskQueue(任务队列):保存等待执行的任务阻塞队列,有多种选择:数组先进先出队列、链表先进先出队列、不存储元素的阻塞队列、含优先级的无限阻塞队列。注意:不要使用无界队列,任务堆积会占用内存空间,一旦占满就会频繁GC,导致服务不可用。
  • RejectedExecutionHandler(饱和策略):队列和线程池都满了后,处于饱和状态时的新任务处理策略。默认是AbortPolicy直接抛出异常,还有直接丢弃、丢弃最近任务等等。

corePoolSize 配置策略?

  • CPU 密集型任务(如计算/图像处理)
    设置为 CPU核数 + 1,如 Runtime.getRuntime().availableProcessors() + 1
  • IO 密集型任务(如数据库/网络请求)
    设置为 CPU核数 * 2 或更高,因线程会长时间等待 IO。
  • 混合型任务:根据工作负载特点调整。

maximumPoolSize 的配置策略?

  • 设置为核心线程数的 2-3 倍,或视任务量峰值调整。
  • 注意:如果 workQueue 为无界队列(如 LinkedBlockingQueue),maximumPoolSize 实际无效。

keepAliveTime 的配置策略?

  • 默认值:60 秒。
  • 适用场景:
    • 短期任务:缩短存活时间,及时释放空闲线程。
    • 长期任务:适当延长,避免频繁创建销毁线程。

线程池配置示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.util.concurrent.*;

public class ThreadPoolExample {

public static void main(String[] args) {
int corePoolSize = Runtime.getRuntime().availableProcessors() + 1;
int maxPoolSize = corePoolSize * 2;
long keepAliveTime = 60;

ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize, // 核心线程数
maxPoolSize, // 最大线程数
keepAliveTime, // 非核心线程存活时间
TimeUnit.SECONDS, // 时间单位
new LinkedBlockingQueue<>(100), // 工作队列
Executors.defaultThreadFactory(),// 线程工厂
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);

// 提交任务
for (int i = 1; i <= 200; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("Task " + taskId + " is running by " + Thread.currentThread().getName());
try {
Thread.sleep(200); // 模拟任务执行
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Task " + taskId + " completed.");
});
}

// 关闭线程池
executor.shutdown();
}
}

推荐配置示例 (生产环境参考):

  • CPU 密集型任务配置示例

    1
    2
    3
    4
    5
    6
    7
    8
    ThreadPoolExecutor cpuExecutor = new ThreadPoolExecutor(
    Runtime.getRuntime().availableProcessors() + 1, // 核心线程数
    Runtime.getRuntime().availableProcessors() + 1, // 最大线程数
    0L, TimeUnit.MILLISECONDS, // 非核心线程存活时间
    new LinkedBlockingQueue<>(100), // 队列大小
    Executors.defaultThreadFactory(),
    new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
    );
  • IO 密集型任务配置示例

    1
    2
    3
    4
    5
    6
    7
    8
    ThreadPoolExecutor ioExecutor = new ThreadPoolExecutor(
    10, // 核心线程数
    100, // 最大线程数
    60, TimeUnit.SECONDS, // 非核心线程存活时间
    new SynchronousQueue<>(), // 无缓冲队列
    Executors.defaultThreadFactory(),
    new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
    );

常见注意事项与最佳实践:

  1. 选择合适的队列与拒绝策略:根据任务数量与执行时间配置队列与策略,避免资源枯竭。
  2. 监控线程池状态:通过 ThreadPoolExecutor 提供的方法监控任务执行与队列长度。
  3. 合理配置最大线程数:避免设置过高,导致过度竞争与资源耗尽。
  4. 拒绝策略选择:
    • AbortPolicy(默认): 抛出异常。
    • CallerRunsPolicy:由调用线程执行任务。
    • DiscardPolicy:直接丢弃任务。
    • DiscardOldestPolicy:丢弃队列中最旧的任务。

问:阻塞队列在生产中的设置?拒绝策略的选择?⭐⭐⭐

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

workQueue 队列类型选择?一般设置为 0,防止用户阻塞。选择的核心因素有:

  • 任务数量:预计任务请求数量和峰值负载。
  • 任务处理时间:短任务或长时间任务。
  • 内存占用:内存是否足够,避免OOM。
  • 任务执行顺序:是否需要按提交顺序处理。
队列类型 描述 适用场景 选择策略
ArrayBlockingQueue 有界、FIFO 队列,数组结构,需指定容量。 任务数量已知,需内存控制。 容量设置:适中,防止OOM。
LinkedBlockingQueue 链表结构,默认无界,FIFO,任务多时自动扩展。 高并发任务,队列长度难预测。IO 密集任务 设置最大容量,避免内存泄漏。
SynchronousQueue 无缓冲区,直接将任务交给工作线程。 实时高频任务,短时间执行。短期异步任务,低延迟 无缓冲队列:高吞吐。适用于高负载请求处理
PriorityBlockingQueue 支持优先级排序的无界队列。 优先级调度,如延迟任务调度。 自定义优先级规则。
DelayQueue 定时任务队列,按到期时间执行。 定时任务执行,如任务调度器。 适用于延迟任务场
策略名称 描述 适用场景 注意事项
AbortPolicy(默认策略) 丢弃任务并抛出 RejectedExecutionException 异常 重要任务需确保不丢失任务。 需捕获异常,避免程序崩溃。
CallerRunsPolicy 由提交任务的线程执行该任务,所有任务都能被执行 允许当前任务回退,任务重要但可延迟。 阻塞提交线程,可能影响响应时间。
DiscardPolicy 直接丢弃任务,无任何通知或异常 不重要的任务或允许任务丢失的场景 任务丢失风险,适用于日志记录。
DiscardOldestPolicy 丢弃队列中最旧任务,然后重试提交被拒绝的新任务 不重要任务的前提下,优先保留最新任务 可能导致旧任务被丢弃。

问:Java中的线程池是如何实现的?讲一下线程池的原理?⭐⭐⭐

  • 线程池是什么?

    • 线程池(Thread Pool)是一种管理线程的工具,通过复用已创建的线程来执行任务,将资源统一在一起管理。
  • 线程池解决的问题是?

    1. 资源消耗高:频繁创建和销毁线程造成的资源损耗巨大。
    2. 资源枯竭风险: 对申请无限制措施,有导致资源耗尽的风险。
    3. 管理复杂度高:系统无法合理的管理资源分布,降低系统稳定性。
  • 在JDK中主要是核心类ThreadPoolExecutor。其设计基于生产者-消费者模型,将任务与线程解耦,实现任务的缓冲与执行。

    • 顶层接口Executor:将任务提交和任务执行进行解耦。用户无需关注如何创建线程,如何调度线程来执行任务,用户只需提供Runnable对象,将任务的运行逻辑提交到执行器(Executor)中,由Executor框架完成线程的调配和任务的执行部分。

    • ExecutorService接口:(1)扩充执行任务的能力,补充可以为一个或一批异步任务生成Future的方法;(2)提供了管控线程池的方法,比如停止线程池的运行。AbstractExecutorService则是上层的抽象类,将执行任务的流程串联了起来,保证下层的实现只需关注一个执行任务的方法即可。

    • 最下层的实现类ThreadPoolExecutor实现最复杂的运行部分。

      关键就是任务和线程的管理。二者解耦,任务管理部分充当生产者的角色,当任务提交后,线程池会判断该任务后续的流转:(1)直接申请线程执行该任务;(2)缓冲到队列中等待线程执行;(3)拒绝该任务。线程管理部分是消费者,它们被统一维护在线程池内,根据任务请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行,最终当线程获取不到任务的时候,线程就会被回收。

      图2 ThreadPoolExecutor运行流程

      • 生命周期管理:线程池内部使用一个变量维护两个值:**运行状态(runState)和线程数量 (workerCount)**。ThreadPoolExecutor使用一个AtomicInteger变量ctl同时记录线程池运行状态和线程数量,避免同步冲突。高3位保存runState,低29位保存workerCount,两个变量之间互不干扰。运行状态包括:RUNNINGSHUTDOWNSTOPTIDYINGTERMINATED

      • 任务执行机制:包括调度、缓冲、申请、拒绝

        1. 调度:所有任务的调度都是由execute方法完成,内容主要为:检查现在线程池的运行状态、运行线程数、运行策略,决定接下来执行的流程,是直接申请线程执行,或是缓冲到队列中执行,亦或是直接拒绝该任务。

          任务执行流程:

          1. 首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。
          2. 如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。
          3. 如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。
          4. 如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。
          5. 如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。

          图4 任务调度流程

        2. 缓冲:线程池通过阻塞队列缓存任务,工作线程从阻塞队列中获取任务,从而实现解耦。

        3. 申请:

          • 任务的执行有两种可能:一种是任务直接由新创建的线程执行。另一种是线程从任务队列中获取任务然后执行,执行完任务的空闲线程会再次去从队列中申请任务再去执行。第一种情况仅出现在线程初始创建的时候,第二种是线程获取任务绝大多数的情况。

          • 线程需要从任务缓存模块中不断地取任务执行,帮助线程从阻塞队列中获取任务,实现线程管理模块和任务管理模块之间的通信。这部分策略由getTask方法实现,其执行流程如下图所示:getTask这部分进行了多次判断,为的是控制线程的数量,使其符合线程池的状态。如果线程池现在不应该持有那么多线程,则会返回null值。工作线程Worker会不断接收新任务去执行,而当工作线程Worker接收不到任务的时候,就会开始被回收。

            图6 获取任务流程图

        4. 拒绝:线程池有一个最大的容量,当线程池的任务缓存队列已满,并且线程池中的线程数目达到maximumPoolSize时,就需要拒绝掉该任务,采取任务拒绝策略,保护线程池。

      • Worker线程管理:线程池为了掌握线程的状态并维护线程的生命周期,设计了线程池内的工作线程Worker。

        • Worker线程模型: 每个Worker表示一个工作线程,包含任务执行逻辑。线程从队列中获取任务,执行完成后继续获取新任务。如果获取任务失败,则终止线程。

          图7 Worker执行任务

        • 线程管理策略:

          • 增加线程:addWorker()方法根据核心/非核心线程池容量添加线程。
          • 线程回收:通过processWorkerExit()方法检测空闲线程,自动回收。
          • 任务拒绝:实现RejectedExecutionHandler接口,自定义拒绝策略。
        • 线程池使用一张Hash表去持有线程的引用,这样可以通过添加引用、移除引用这样的操作来控制线程的生命周期。

        • 判断线程是否正在运行:Worker是通过继承AQS,使用AQS来实现独占锁这个功能。没有使用可重入锁ReentrantLock,而是使用AQS,为的就是实现不可重入的特性去反应线程现在的执行状态。

        • 1.lock方法一旦获取了独占锁,表示当前线程正在执行任务中。 2.如果正在执行任务,则不应该中断线程。 3.如果该线程现在不是独占锁的状态,也就是空闲的状态,说明它没有在处理任务,这时可以对该线程进行中断。 4.线程池在执行shutdown方法或tryTerminate方法时会调用

        • interruptIdleWorkers方法来中断空闲的线程,interruptIdleWorkers方法会使用tryLock方法来判断线程池中的线程是否是空闲状态;如果线程是空闲状态则可以安全回收。在线程回收过程中就使用到了这种特性,回收过程如下图所示:

          图8 线程池回收过程

        • 增加线程:增加线程是通过线程池中的addWorker方法,该方法的功能就是增加一个线程,该方法不考虑线程池是在哪个阶段增加的该线程,这个分配线程的策略是在上个步骤完成的,该步骤仅仅完成增加线程,并使它运行,最后返回是否成功这个结果。addWorker方法有两个参数:firstTask、core。firstTask参数用于指定新增的线程执行的第一个任务,该参数可以为空;core参数为true表示在新增线程时会判断当前活动线程数是否少于corePoolSize,false表示新增线程前需要判断当前活动线程数是否少于maximumPoolSize,其执行流程如下图所示:

          图9 申请线程执行流程图

          图9 申请线程执行流程图

        • 线程回收

        • 线程池中线程的销毁依赖JVM自动的回收,线程池做的工作是根据当前线程池的状态维护一定数量的线程引用,防止这部分线程被JVM回收,当线程池决定哪些线程需要回收时,只需要将其引用消除即可。Worker被创建出来后,就会不断地进行轮询,然后获取任务去执行,核心线程可以无限等待获取任务,非核心线程要限时获取任务。当Worker无法获取到任务,也就是获取的任务为空时,循环会结束,Worker会主动消除自身在线程池内的引用。

          1
          2
          3
          4
          5
          6
          7
          try {
          while (task != null || (task = getTask()) != null) {
          //执行任务
          }
          } finally {
          processWorkerExit(w, completedAbruptly);//获取不到任务时,主动回收自己
          }

          线程回收的工作是在processWorkerExit方法完成的。

          图10 线程销毁流程

          事实上,在这个方法中,将线程引用移出线程池就已经结束了线程销毁的部分。但由于引起线程销毁的可能性有很多,线程池还要判断是什么引发了这次销毁,是否要改变线程池的现阶段状态,是否要根据新状态,重新分配线程。

        • 执行任务:在Worker类中的run方法调用了runWorker方法来执行任务,runWorker方法的执行过程如下:

          1.while循环不断地通过getTask()方法获取任务。 2.getTask()方法从阻塞队列中取任务。 3.如果线程池正在停止,那么要保证当前线程是中断状态,否则要保证当前线程不是中断状态。 4.执行任务。 5.如果getTask结果为null则跳出循环,执行processWorkerExit()方法,销毁线程。

          执行流程如下图所示:

          图11 执行任务流程

  • 线程中线程被抽象为静态内部类Worker,是基于AQS实现的存放在HashSet中;

  • 要被执行的线程存放在BlockingQueue中;

  • 基本思想就是从workQueue中取出要执行的任务,放在worker中处理;

  • 提交一个任务,线程池里存活的核心线程数小于corePoolSize时,线程池会创建一个核心线程去处理提交的任务
  • 如果线程池核心线程数已满,即线程数已经等于corePoolSize,一个新提交的任务,会被放进任务队列workQueue排队等待执行。
  • 当线程池里面存活的线程数已经等于corePoolSize了,并且任务队列workQueue也满,判断线程数是否达到maximumPoolSize,即最大线程数是否已满,如果没到达,创建非核心线程执行提交的任务。
  • 如果当前的线程数达到了maximumPoolSize,还有新的任务过来的话,直接采用拒绝策略处理。

问:如果线程池中的一个线程运行时出现了异常,会发生什么?

如果提交任务的时候使用了submit,则返回的feature里会存有异常信息,但是如果数execute则会打印出异常栈。但是不会给其他线程造成影响。之后线程池会删除该线程,会新增加一个worker。

当一个线程池里面的线程异常后:

  1. 当执行方式是execute时,可以看到堆栈异常的输出。
    • 执行在ThreadPoolExecutor.runWorker()方法中#task.run(),如果异常的话会直接throw所以可以看到异常堆栈输出。
  2. 当执行方式是submit时,异常堆栈没有输出。但是调用Future.get()方法时,可以捕获到异常。
    • 原因:submit会先把任务封装为FutureTask,在调用ThreadPoolExecutor.runWorker()方法#task.run()后,还会继续执行FutureTask.run()方法,报错后会setException(ex)将异常存入,并没有抛出异常。直到调用Future.get()时才会将异常抛出。
  3. 不会影响线程池里面其他线程的正常执行。
  4. 线程池会把这个线程移除掉,并创建一个新的线程放到线程池中。当线程异常,会调用ThreadPoolExecutor.runWorker()方法最后面的finally中的processWorkerExit(),会将此线程remove,并重新addworker()一个线程。

问:如何实现一个线程池,Java中线程池如何进行配置?线程池的线程数怎么确定?线程池的工作流程记得吗?如果是IO操作为主怎么确定?如果计算型操作又怎么确定?跳表的查询过程是怎么样的,查询和插入的时间复杂度?⭐⭐

线程池的线程数怎么确定?

  1. I/O 密集型任务:
    • I/O 操作等待时间较长,CPU 大部分时间在等待 I/O 操作完成。
    • 线程数公式: 线程数=CPU 核心数×2\text{线程数} = \text{CPU 核心数} \times 2线程数=CPU 核心数×2
  2. 计算密集型任务:
    • 计算密集型任务几乎完全占用 CPU。
    • 线程数公式: 线程数=CPU 核心数+1\text{线程数} = \text{CPU 核心数} + 1线程数=CPU 核心数+1
  3. 混合型任务:
    • 根据任务性质自定义线程池,适当增加线程数。

线程池的工作流程记得吗?

  1. 提交任务: 调用 execute()submit() 方法提交任务。
  2. 判断线程数:
    • 如果运行线程数 < corePoolSize,创建新线程。
    • 如果运行线程数 ≥ corePoolSize,将任务加入 workQueue
    • 如果 workQueue 满了,且线程数 < maximumPoolSize,则创建新线程。
    • 如果线程数达到 maximumPoolSize,执行拒绝策略。
  3. 执行任务: 使用工作线程从队列中取任务并执行。
  4. 线程回收: 空闲线程超过 keepAliveTime 时销毁。
  5. 线程池终止: 调用 shutdown()shutdownNow()

跳表的查询过程是怎么样的,查询和插入的时间复杂度?

  1. 概述:跳表是一种多层链表结构,支持快速查找、插入和删除操作,是 RedisConcurrentSkipListMap 的基础结构。
  2. 查询过程:假设要查询目标值 target:
    1. 从最高层链表头节点开始。
    2. 比较当前节点值:
      • 如果当前节点值 < target,向右移动。
      • 如果当前节点值 ≥ target 或到达层末尾,向下移动一层。
    3. 继续上述过程,直到找到目标节点或遍历结束。
  3. 插入过程:假设插入值为 value:
    1. 从最高层开始,找到小于 value 的最后一个节点。
    2. 逐层插入节点,并根据随机概率决定是否增加节点高度。
    3. 如果节点高度超过当前最大高度,增加跳表高度。
  4. 时间复杂度:
操作 平均时间复杂度 最坏时间复杂度
查询 O(log n) O(n)
插入 O(log n) O(n)
删除 O(log n) O(n)

问:线程池里如何知道线程执行完了没有?线程阻塞怎么办?怎么保证所有线程执行完之后继续往下处理?怎么让一个线程等另一个线程执行结束?⭐⭐⭐

  • 线程池里如何知道线程执行完了没有?

    • isTerminated 方法:利用线程池的终止状态(TERMINATED)来判断线程池的任务是否已经全部执行完。需要调用线程池的 shutdown 方法,不然线程池一直会处于 RUNNING 运行状态。问题是需要关闭线程池。

    • getCompletedTaskCount 方法:判断线程池中的计划执行任务数和已完成任务数 threadPool.getTaskCount() != threadPool.getCompletedTaskCount()。但问题是两个方法返回的值是动态变化的不是一个准确的数值。

    • Future接口:该接口可以判断单个任务是否执行完成。

      • ExecutorService.submit(Callable/Void) 方法会返回 Future 对象。
      • future.isDone() 轮询任务状态,直到完成。
      • future.get() 获取任务结果,如果未完成会阻塞。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
          
      public static void main(String[] args) throws InterruptedException {
      ExecutorService executor = Executors.newFixedThreadPool(3);
      List<Future<String>> futures = new ArrayList<>();

      // 提交任务
      for (int i = 0; i < 5; i++) {
      final int taskId = i;
      futures.add(executor.submit(() -> {
      Thread.sleep(1000); // 模拟任务
      return "任务 " + taskId + " 完成";
      }));
      }

      // 检查是否所有任务完成
      boolean allDone = false;
      while (!allDone) {
      allDone = futures.stream().allMatch(Future::isDone);
      System.out.println("是否全部完成: " + allDone);
      Thread.sleep(500); // 轮询间隔
      }
      // 或者通过executor.invokeAll一次性全部提交所有任务

      // 获取任务结果
      for (Future<String> future : futures) {
      try {
      System.out.println(future.get());
      } catch (ExecutionException e) {
      e.printStackTrace();
      }
      }

      executor.shutdown();
      }
    • CountDownLatch:使用其作为计数器,初始为任务数N,每完成一个任务就-1,为0表示全部执行完。缺点是CountDownLatch只能被使用一次。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      public static void main(String[] args) throws InterruptedException {
      int taskCount = 5;
      CountDownLatch latch = new CountDownLatch(taskCount);
      ExecutorService executor = Executors.newFixedThreadPool(3);

      for (int i = 0; i < taskCount; i++) {
      final int taskId = i;
      executor.execute(() -> {
      System.out.println("执行任务:" + taskId);
      try {
      Thread.sleep(1000); // 模拟任务执行
      } catch (InterruptedException e) {
      e.printStackTrace();
      }
      latch.countDown(); // 任务完成,计数减一
      });
      }

      System.out.println("等待所有任务完成...");
      latch.await(); // 等待计数器归零
      System.out.println("所有任务完成!");
      executor.shutdown();
      }
    • CyclicBarrier:则可以循环重复使用,通过reset方法重置。适用于多个线程到达同步点时相互等待,支持重用。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
          public static void main(String[] args) {
      int threadCount = 3;
      CyclicBarrier barrier = new CyclicBarrier(threadCount, () ->
      System.out.println("所有线程到达屏障点,继续执行...")
      );

      for (int i = 0; i < threadCount; i++) {
      final int threadId = i;
      new Thread(() -> {
      try {
      System.out.println("线程 " + threadId + " 正在执行任务...");
      Thread.sleep((long) (Math.random() * 3000));
      System.out.println("线程 " + threadId + " 到达屏障点");
      barrier.await(); // 等待其他线程
      System.out.println("线程 " + threadId + " 开始执行下一阶段任务");
      } catch (Exception e) {
      e.printStackTrace();
      }
      }).start();
      }
      }
      // 主线程不参与等待,barrier.await() 会阻塞线程,直到所有线程都到达屏障点。
      // Runnable 动作在所有线程到达屏障时触发。
  • 线程阻塞怎么办?

    1. 调整线程池配置,置合理的 corePoolSizemaximumPoolSizequeueCapacity避免核心线程被长时间占用,使用 CachedThreadPool 适合短时任务。使用 ThreadPoolExecutor 配置 keepAliveTime 控制闲置线程销毁。

    2. 使用超时控制机制。设置超时时间Future.get(timeout))。**future.cancel(true)** 取消任务。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      public static void main(String[] args) {
      ExecutorService executor = Executors.newFixedThreadPool(2);

      Callable<String> task = () -> {
      Thread.sleep(5000); // 模拟长时间任务
      return "任务完成";
      };

      Future<String> future = executor.submit(task);
      try {
      System.out.println(future.get(3, TimeUnit.SECONDS)); // 超时设置为3秒
      } catch (TimeoutException e) {
      System.out.println("任务超时,取消任务");
      future.cancel(true); // 超时后取消任务
      } catch (Exception e) {
      e.printStackTrace();
      }
      executor.shutdown();
      }
    3. 优化锁管理与同步机制:尽量使用无锁设计(如 ConcurrentHashMapAtomic 类)。使用 ReentrantLock 并启用公平锁避免饥饿。

    4. 使用异步框架,如 CompletableFuture 或 Reactor,减少阻塞等待:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      public class AsyncExample {
      public static void main(String[] args) {
      CompletableFuture.supplyAsync(() -> {
      try {
      Thread.sleep(2000);
      } catch (InterruptedException e) {
      e.printStackTrace();
      }
      return "异步任务完成";
      }).thenAccept(result -> System.out.println(result));
      System.out.println("主线程继续执行...");
      }
      }
  • 怎么保证所有线程执行完之后继续往下处理?

    • 使用 awaitTermination() 方法:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      ExecutorService executor = Executors.newFixedThreadPool(3);
      for (int i = 0; i < 5; i++) {
      final int taskId = i;
      executor.execute(() -> {
      System.out.println("执行任务:" + taskId);
      });
      }
      executor.shutdown();
      try {
      if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
      System.out.println("超时未完成");
      } else {
      System.out.println("所有任务完成");
      }
      } catch (InterruptedException e) {
      e.printStackTrace();
      }
    • 使用 CountDownLatch

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      int taskCount = 5;
      CountDownLatch latch = new CountDownLatch(taskCount);
      ExecutorService executor = Executors.newFixedThreadPool(3);

      for (int i = 0; i < taskCount; i++) {
      final int taskId = i;
      executor.execute(() -> {
      System.out.println("执行任务:" + taskId);
      latch.countDown(); // 减少计数
      });
      }

      try {
      latch.await(); // 等待任务完成
      System.out.println("所有任务完成");
      } catch (InterruptedException e) {
      e.printStackTrace();
      } finally {
      executor.shutdown();
      }
  • 怎么让一个线程等另一个线程执行结束?

    • 使用 join() 方法:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      Thread t1 = new Thread(() -> {
      System.out.println("线程1开始");
      try {
      Thread.sleep(2000);
      } catch (InterruptedException e) {
      e.printStackTrace();
      }
      System.out.println("线程1结束");
      });

      Thread t2 = new Thread(() -> {
      System.out.println("线程2等待线程1执行完成");
      try {
      t1.join(); // 等待线程1完成
      } catch (InterruptedException e) {
      e.printStackTrace();
      }
      System.out.println("线程2开始执行");
      });

      t1.start();
      t2.start();
    • 使用 Future.get()

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      ExecutorService executor = Executors.newSingleThreadExecutor();
      Future<String> future = executor.submit(() -> {
      Thread.sleep(3000);
      return "任务完成";
      });

      try {
      System.out.println("等待任务结果...");
      String result = future.get(); // 阻塞等待结果
      System.out.println("任务结果:" + result);
      } catch (InterruptedException | ExecutionException e) {
      e.printStackTrace();
      } finally {
      executor.shutdown();
      }
    • CountDownLatch、CyclicBarrier

问:Java 的信号灯?⭐

  • 信号灯 (Semaphore) 是一种同步机制,属于 java.util.concurrent 包。它用于控制对共享资源的访问,常用于实现限流连接池管理资源分配等场景。
  • 构造方法:
    • Semaphore(int permits):创建一个具有指定许可数的信号灯,默认非公平。
    • Semaphore(int permits, boolean fair):指定许可数和公平性。
  • 重要方法:
    • acquire():获取许可,若无可用许可则阻塞。
    • acquire(int permits):获取多个许可。
    • release():释放许可,增加可用许可数。
    • release(int permits):释放多个许可。
    • availablePermits():查询剩余许可数量。
    • tryAcquire():尝试获取许可,立即返回 truefalse
  • 信号灯的工作机制:
    • 许可数: 表示资源数量。
    • 公平性:
      • 公平信号灯: 按请求的先后顺序分配许可,类似于队列管理。
      • 非公平信号灯: 可能会优先分配给新请求,吞吐量较高。
  • 使用示例: 限制并发访问,场景: 模拟停车场管理,最多允许三个车辆进入。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.util.concurrent.Semaphore;

public class ParkingLot {
// 最多容纳 3 辆车
private static final Semaphore parkingSlots = new Semaphore(3);

public static void main(String[] args) {
for (int i = 1; i <= 6; i++) {
final int car = i;
new Thread(() -> {
try {
System.out.println("Car " + car + " is trying to enter.");
parkingSlots.acquire(); // 获取许可
System.out.println("Car " + car + " has parked.");
Thread.sleep(2000); // 模拟停车时间
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
parkingSlots.release(); // 释放许可
System.out.println("Car " + car + " has left.");
}
}).start();
}
}
}
  • 适用场景与应用:
    1. 限流控制: 控制访问共享资源的最大并发数(如 API 调用限流)。
    2. 资源池管理: 数据库连接池或对象池管理。
    3. 多线程任务管理: 控制线程运行数量,防止资源耗尽。
  • 注意事项:
    • 公平性权衡: 公平性降低了性能,但保证了任务公平执行。
    • 资源正确释放: 必须在 finally 块中释放许可,防止死锁。
    • 许可数量动态调整: 可根据负载情况调整许可数量,支持灵活扩展。

问:Executors 静态方法?

Executors 是 Java 中的一个工具类,提供了创建线程池的静态方法。以下是常用的 Executors 静态方法:

  1. 创建线程池方法:
方法 描述
newFixedThreadPool(int nThreads) 创建一个固定大小的线程池,最多可同时运行 nThreads 个线程。
newCachedThreadPool() 创建一个可根据需要创建新线程的线程池(无限制),适用于执行大量短期异步任务。
newSingleThreadExecutor() 创建一个单线程的线程池,所有任务将被顺序执行。
newScheduledThreadPool(int nThreads) 创建一个线程池,可用于定时或周期性任务。
newWorkStealingPool(int parallelism) 创建一个基于工作窃取的线程池,适用于并行任务(Java 8+)。

2. 定时任务方法

这些方法返回 ScheduledExecutorService,用于延时和周期性执行任务。

方法 描述
schedule(Runnable command, long delay, TimeUnit unit) 在指定延时后执行一次任务。
schedule(Callable<V> callable, long delay, TimeUnit unit) 在延时后执行任务并返回结果。
scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) 按固定周期执行任务,从任务开始时间算起。
scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) 按固定延时执行任务,从上次任务完成时间算起。

3. 关闭线程池方法

方法 描述
shutdown() 优雅关闭线程池,等待已提交的任务完成。
shutdownNow() 立即停止所有正在执行的任务,返回未执行的任务列表。
awaitTermination(long timeout, TimeUnit unit) 阻塞直到所有任务完成或超时。

1.5 线程安全

问:谈一下对线程安全的理解?用什么方法保证线程的安全?

  • 谈一下对线程安全的理解?

    • 线程安全是指在多线程环境中,多个线程访问共享资源时,程序能够正确运行,不会产生数据不一致或其他不可预期的行为。

      在多线程环境中,线程的交替执行可能导致 竞态条件(Race Condition)数据竞争(Data Race)死锁(Deadlock) 等问题。因此,需要采取适当的机制来确保线程安全。

  • 用什么方法保证线程的安全?

    1. 使用不可变对象,如String, Integer, BigDecimal, LocalDateTime
    2. 使用锁机制:同步代码块(synchronized)、ReentrantLock等。
    3. 使用原子变量:java.util.concurrent.atomic 包中的类,如 AtomicIntegerAtomicLong,实现无锁线程安全操作。
    4. 使用 java.util.concurrent 中的线程安全集合,如:ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentLinkedQueue等。
    5. 使用 Java 提供的并发工具类,如 CountDownLatch, CyclicBarrier, Semaphore, ReadWriteLock, ExecutorService 等,确保线程协调和同步。
  • 选择线程安全方案的原则:

    1. 数据不可变时,优先考虑不可变类
    2. 数据量小、操作简单时,考虑同步方法或代码块
    3. 多线程频繁访问时,使用并发集合或原子变量
    4. 有复杂同步需求时,使用高级并发工具类
    5. 无共享数据时,考虑 ThreadLocal

问:如何实现一个线程安全的计数器?

实现方式 适用场景 优点 缺点
synchronized 简单计数,低并发 简单易用 性能受限于锁机制
ReentrantLock 复杂同步场景 灵活,功能强大 锁管理复杂
AtomicInteger 中低并发环境,简单计数 高性能,无锁实现 功能有限
LongAdder 高并发环境,频繁更新 高性能,分段计数 读取值略复杂
ConcurrentHashMap 多键计数,实时统计 自动线程安全 内存占用较大

1. 使用 synchronized 关键字:将关键方法或代码块同步,确保同一时刻只有一个线程执行。

1
2
3
4
5
6
7
8
9
10
11
class SynchronizedCounter {
private int count = 0;

public synchronized void increment() {
count++;
}

public synchronized int getCount() {
return count;
}
}

特点:

  • 简单直接。
  • 性能受限于锁的粒度。

**2. 使用 ReentrantLock**:通过显式锁,灵活控制锁的获取和释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.concurrent.locks.ReentrantLock;

class ReentrantLockCounter {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();

public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}

public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}

特点:

  • 更灵活,可实现公平锁或非公平锁。
  • 手动控制锁,代码复杂度增加。

3. 使用 AtomicInteger(推荐)AtomicInteger 是无锁的线程安全实现,基于 CAS(Compare-And-Swap)机制。

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.concurrent.atomic.AtomicInteger;

class AtomicCounter {
private final AtomicInteger count = new AtomicInteger(0);

public void increment() {
count.incrementAndGet();
}

public int getCount() {
return count.get();
}
}

特点:

  • 无锁实现,性能高。
  • 适用于计数操作较为简单的场景。

4. 使用 LongAdder(高并发推荐)LongAdderAtomicLong 的改进版本,适合高并发环境。

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.concurrent.atomic.LongAdder;

class LongAdderCounter {
private final LongAdder count = new LongAdder();

public void increment() {
count.increment();
}

public long getCount() {
return count.sum();
}
}

特点:

  • 在高并发场景中性能优于 AtomicInteger
  • 适合频繁更新但读取不多的场景。

5. 使用 ConcurrentHashMap(计数表):适用于统计多个计数值,如网站访问量或实时统计。

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.concurrent.ConcurrentHashMap;

class ConcurrentMapCounter {
private final ConcurrentHashMap<String, Integer> counter = new ConcurrentHashMap<>();

public void increment(String key) {
counter.merge(key, 1, Integer::sum);
}

public int getCount(String key) {
return counter.getOrDefault(key, 0);
}
}

特点:

  • 适合多键值计数。
  • 无需显式同步。

问:请写一个线程安全的单例模式?饿汉式,懒汉式等。两次判断 instance 是否为空,每次判断的作用是什么?

实现方式 线程安全 延迟加载 简洁性 性能
饿汉式 简单
懒汉式(双重检查) 一般
静态内部类 简单
枚举实现 否(但不浪费) 简单

1. 饿汉式(静态初始化):线程安全,类加载时创建实例。

1
2
3
4
5
6
7
8
9
public class SingletonEager {
private static final SingletonEager INSTANCE = new SingletonEager();

private SingletonEager() {}

public static SingletonEager getInstance() {
return INSTANCE;
}
}

特点:

  • 优点: 实现简单,线程安全。
  • 缺点: 类加载时就初始化,可能造成资源浪费。

2. 懒汉式(双重检查锁定):延迟加载,确保实例只在第一次访问时创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SingletonLazy {
private static volatile SingletonLazy instance;

private SingletonLazy() {}

public static SingletonLazy getInstance() {
if (instance == null) {
synchronized (SingletonLazy.class) {
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
}

特点:

  • 优点: 延迟加载,线程安全,性能较好。
  • 缺点: 实现稍复杂,需使用 volatile 防止指令重排序。

3. 静态内部类(推荐):利用类加载机制,延迟加载,线程安全。

1
2
3
4
5
6
7
8
9
10
11
public class SingletonHolder {
private SingletonHolder() {}

private static class Holder {
private static final SingletonHolder INSTANCE = new SingletonHolder();
}

public static SingletonHolder getInstance() {
return Holder.INSTANCE;
}
}

特点:

  • 优点: 延迟加载,线程安全,实现简单。
  • 缺点: 依赖于类加载机制,适用于大多数情况。

4. 枚举实现(最推荐):天生线程安全,防止反序列化和反射攻击。

1
2
3
4
5
6
7
public enum SingletonEnum {
INSTANCE;

public void doSomething() {
System.out.println("Singleton using Enum");
}
}

特点:

  • 优点: 简洁,线程安全,防止序列化和反射攻击。
  • 缺点: 适用于单例对象场景,扩展性稍差。

在双重检查锁定的懒汉式单例实现中,instance 被检查了两次。每次检查的作用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Singleton {
// 使用 volatile 确保可见性和防止指令重排序
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;
}
}

为什么需要两次检查?

  1. 第一次检查 (不加锁):if (instance == null)

    • 作用: 避免不必要的加锁。
    • 原因: 单例对象通常只需要创建一次。在大多数情况下,instance 已经被初始化,这样可以跳过锁定过程,提高性能。
  2. 第二次检查 (加锁内):

    1
    2
    3
    4
    5
    synchronized (Singleton.class) {
    if (instance == null) {
    instance = new Singleton();
    }
    }
    • 作用: 确保对象的唯一性,防止多线程竞争时重复创建实例。
    • 原因: 多个线程可能在第一次检查时都看到 instance == null,因此进入同步块。在同步块中再次检查,确保只有第一个到达的线程初始化实例,其余线程不会重复创建。
  3. 总结:双重检查锁定确保了 线程安全性能优化 的平衡,是一种推荐的懒汉式单例实现方式。

    • 没有第一次检查: 每次获取实例时都会加锁,性能较低。
    • 没有第二次检查: 多个线程可能同时通过第一次检查,导致重复创建实例,破坏单例模式的唯一性。

问:ABC 三个线程如何保证顺序执行?

选择适合的方案:

  • 简单控制: 使用 join()
  • 多线程同步: 使用 CountDownLatchSemaphore
  • 精确控制: 使用 ReentrantLockCondition
  1. 使用 join():主线程启动 A,等待 A 结束,再启动 B,等待 B 结束,最后启动 C。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class JoinExample {
    public static void main(String[] args) throws InterruptedException {
    Thread threadA = new Thread(() -> System.out.println("A"));
    Thread threadB = new Thread(() -> System.out.println("B"));
    Thread threadC = new Thread(() -> System.out.println("C"));

    threadA.start();
    threadA.join(); // 等待 A 执行完

    threadB.start();
    threadB.join(); // 等待 B 执行完

    threadC.start();
    threadC.join(); // 等待 C 执行完
    }
    }
  2. **使用 CountDownLatch**:使用 CountDownLatch 控制线程的启动顺序。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    import java.util.concurrent.CountDownLatch;

    public class CountDownLatchExample {
    public static void main(String[] args) {
    CountDownLatch latchA = new CountDownLatch(0); // A 无需等待
    CountDownLatch latchB = new CountDownLatch(1); // B 等待 A
    CountDownLatch latchC = new CountDownLatch(1); // C 等待 B

    Thread threadA = new Thread(() -> {
    System.out.println("A");
    latchB.countDown(); // 通知 B 开始
    });

    Thread threadB = new Thread(() -> {
    try {
    latchB.await(); // 等待 A 完成
    System.out.println("B");
    latchC.countDown(); // 通知 C 开始
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    }
    });

    Thread threadC = new Thread(() -> {
    try {
    latchC.await(); // 等待 B 完成
    System.out.println("C");
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    }
    });

    threadA.start();
    threadB.start();
    threadC.start();
    }
    }
  3. 使用 SemaphoreSemaphore 控制线程的执行时机。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    import java.util.concurrent.Semaphore;

    public class SemaphoreExample {
    public static void main(String[] args) {
    Semaphore semA = new Semaphore(1); // A 可直接执行
    Semaphore semB = new Semaphore(0); // B 等待 A
    Semaphore semC = new Semaphore(0); // C 等待 B

    Thread threadA = new Thread(() -> {
    try {
    semA.acquire();
    System.out.println("A");
    semB.release(); // 通知 B
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    }
    });

    Thread threadB = new Thread(() -> {
    try {
    semB.acquire();
    System.out.println("B");
    semC.release(); // 通知 C
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    }
    });

    Thread threadC = new Thread(() -> {
    try {
    semC.acquire();
    System.out.println("C");
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    }
    });

    threadA.start();
    threadB.start();
    threadC.start();
    }
    }
  4. 使用 ReentrantLock + Condition :使用 ReentrantLockCondition 精确控制线程执行时机。

问:生产者消费者模式的实现方式?

生产者消费者模式通过共享缓冲区在生产者和消费者之间传递数据,常用的实现方式包括以下几种:

  1. wait / notify 实现 (经典方法):使用 synchronized 锁,wait() 等待,notify() 唤醒。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    import java.util.LinkedList;
    import java.util.Queue;

    class Buffer {
    private final Queue<Integer> queue = new LinkedList<>();
    private final int CAPACITY = 5;

    public synchronized void produce(int value) throws InterruptedException {
    while (queue.size() == CAPACITY) {
    wait(); // 缓冲区满,等待
    }
    queue.offer(value);
    System.out.println("Produced: " + value);
    notifyAll(); // 通知消费者
    }

    public synchronized int consume() throws InterruptedException {
    while (queue.isEmpty()) {
    wait(); // 缓冲区空,等待
    }
    int value = queue.poll();
    System.out.println("Consumed: " + value);
    notifyAll(); // 通知生产者
    return value;
    }
    }

    public class ProducerConsumerWaitNotify {
    public static void main(String[] args) {
    Buffer buffer = new Buffer();

    Runnable producer = () -> {
    for (int i = 0; i < 10; i++) {
    try {
    buffer.produce(i);
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    }
    }
    };

    Runnable consumer = () -> {
    for (int i = 0; i < 10; i++) {
    try {
    buffer.consume();
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    }
    }
    };

    new Thread(producer).start();
    new Thread(consumer).start();
    }
    }
  2. BlockingQueue 实现 (推荐方法):使用 BlockingQueue,生产者直接放入,消费者直接取出,自动阻塞管理。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    import java.util.concurrent.ArrayBlockingQueue;
    import java.util.concurrent.BlockingQueue;

    public class ProducerConsumerBlockingQueue {
    public static void main(String[] args) {
    BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(5);

    Runnable producer = () -> {
    for (int i = 0; i < 10; i++) {
    try {
    queue.put(i); // 自动阻塞
    System.out.println("Produced: " + i);
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    }
    }
    };

    Runnable consumer = () -> {
    for (int i = 0; i < 10; i++) {
    try {
    int value = queue.take(); // 自动阻塞
    System.out.println("Consumed: " + value);
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    }
    }
    };

    new Thread(producer).start();
    new Thread(consumer).start();
    }
    }
  3. Lock + Condition 实现 (手动控制):使用 ReentrantLockCondition 对生产与消费条件进行手动控制。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    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;

    class BufferWithLock {
    private final Queue<Integer> queue = new LinkedList<>();
    private final int CAPACITY = 5;
    private final Lock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();

    public void produce(int value) throws InterruptedException {
    lock.lock();
    try {
    while (queue.size() == CAPACITY) {
    notFull.await(); // 缓冲区满,等待
    }
    queue.offer(value);
    System.out.println("Produced: " + value);
    notEmpty.signalAll(); // 通知消费者
    } finally {
    lock.unlock();
    }
    }

    public int consume() throws InterruptedException {
    lock.lock();
    try {
    while (queue.isEmpty()) {
    notEmpty.await(); // 缓冲区空,等待
    }
    int value = queue.poll();
    System.out.println("Consumed: " + value);
    notFull.signalAll(); // 通知生产者
    return value;
    } finally {
    lock.unlock();
    }
    }
    }

    public class ProducerConsumerLock {
    public static void main(String[] args) {
    BufferWithLock buffer = new BufferWithLock();

    Runnable producer = () -> {
    for (int i = 0; i < 10; i++) {
    try {
    buffer.produce(i);
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    }
    }
    };

    Runnable consumer = () -> {
    for (int i = 0; i < 10; i++) {
    try {
    buffer.consume();
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    }
    }
    };

    new Thread(producer).start();
    new Thread(consumer).start();
    }
    }

选择合适实现的场景:

  1. 简单实现: 使用 BlockingQueue,推荐日常开发。
  2. 手动控制: 使用 wait / notifyLock + Condition,适合需要精确控制的情况。

关键点总结:

  • 缓冲区满时,生产者需等待;缓冲区空时,消费者需等待。
  • BlockingQueue 内置阻塞机制,简化了开发。
  • Lock + Condition 提供更灵活的控制,适合复杂场景。

二. 锁

2.1 锁

问:讲讲你知道的锁?锁的几种特性?⭐⭐

  • 独占/排他与共享:独占/排他即同一时刻只能有一个线程获取到锁,而其他获取锁的线程只能处于同步队列中等待,只有获取锁的线程释放了锁,后继的线程才能够获取锁。共享则表示同一时刻多个线程可以同时访问。
  • 公平与非公平:先对锁进行获取请求的线程一定先被满足,这是公平锁;反之则是非公平锁。
  • 可重入:支持重进入,表示锁能够支持一个线程对资源重复加锁。

在 Java 中,锁(Lock)是一种用于控制多个线程对共享资源访问的机制。锁的主要作用是防止数据竞争和线程安全问题。

一、常见的锁类型

  1. 内置锁 (synchronized)
  • Java 提供的内置锁,通过 synchronized 关键字实现。
  • 特点:
    • 自动获取和释放锁。
    • 可重入。
    • 无法中断。
  1. 显式锁 (Lock 接口)
  • java.util.concurrent.locks 包中,主要实现类为 ReentrantLock
  • 特点:
    • 手动加锁和解锁。
    • 可中断。
    • 支持公平锁和非公平锁。
    • 支持尝试加锁(tryLock())。

二、常用锁实现

  1. ReentrantLock(可重入锁)

    • 支持公平和非公平模式。

    • 可重入,锁持有者可以再次获取锁。

    • 提供 lockInterruptibly() 支持线程中断。

    • 示例:

      1
      2
      3
      4
      5
      6
      7
      8
      javaCopy codeLock lock = new ReentrantLock();

      lock.lock();
      try {
      System.out.println("Critical Section");
      } finally {
      lock.unlock();
      }
  2. ReadWriteLock(读写锁)

    • 提供 ReentrantReadWriteLock 实现。

    • 支持读写分离:多个线程可同时读,写操作独占。

    • 示例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      javaCopy codeReadWriteLock lock = new ReentrantReadWriteLock();
      Lock readLock = lock.readLock();
      Lock writeLock = lock.writeLock();

      readLock.lock();
      try {
      System.out.println("Reading...");
      } finally {
      readLock.unlock();
      }
  3. StampedLock(乐观锁)

    • 主要用于高并发场景,支持乐观读和悲观写。

    • 乐观锁通过时间戳版本检查数据是否被修改。

    • 示例:

      1
      2
      3
      4
      5
      6
      7
      javaCopy codeStampedLock lock = new StampedLock();
      long stamp = lock.readLock();
      try {
      System.out.println("Reading...");
      } finally {
      lock.unlockRead(stamp);
      }
  4. Semaphore(信号量)

    • 用于控制资源访问的许可数量,常用于限流。

    • 示例:

      1
      2
      3
      4
      5
      6
      7
      javaCopy codeSemaphore semaphore = new Semaphore(3);  // 最多允许3个线程同时访问
      semaphore.acquire(); // 获取许可
      try {
      System.out.println("Accessing Resource");
      } finally {
      semaphore.release(); // 释放许可
      }
  5. CountDownLatch(倒计时器)

    • 用于等待多个线程完成任务后再继续执行。

    • 示例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      javaCopy codeCountDownLatch latch = new CountDownLatch(3);  // 等待3个线程

      Runnable task = () -> {
      System.out.println("Task completed");
      latch.countDown(); // 减少计数
      };

      new Thread(task).start();
      new Thread(task).start();
      new Thread(task).start();

      latch.await(); // 等待计数变为0
      System.out.println("All tasks finished.");

三、锁的主要特性

特性 描述
互斥性 (Exclusive) 同一时刻只能有一个线程持有锁。
可重入性 (Reentrant) 锁的持有者可以多次获取同一把锁而不被阻塞。
公平性 (Fairness) 锁的获取顺序按先到先得的原则(如 ReentrantLock(true))。
中断响应 (Interruptible) 支持线程在等待锁时被中断。
超时获取 (Timeout) 尝试在指定时间内获取锁,超时则放弃。
读写分离 (Read-Write) 支持多个读线程并行,写线程独占。

四、锁的选择指南

  1. 简单互斥控制: 使用 synchronized
  2. 复杂并发控制: 使用 ReentrantLock
  3. 读写分离: 使用 ReentrantReadWriteLock
  4. 高并发和乐观读: 使用 StampedLock
  5. 资源限制控制: 使用 Semaphore
  6. 任务协调: 使用 CountDownLatchCyclicBarrier

五、注意事项

  • 尽量缩小锁的作用域,避免死锁和性能瓶颈。
  • 谨慎使用嵌套锁,防止锁顺序不一致导致死锁。
  • 在必要时使用超时和中断机制,避免线程无限等待。

问:公平锁与非公平锁?

  1. 公平锁指在分配锁前检查是否有线程在排队等待获取该锁,优先分配排队时间最长的线程,非公平直接尝试获取锁 。
  2. 公平锁需多维护一个锁线程队列,大量的线程切换,效率低;默认非公平。

公平锁和非公平锁是线程锁机制中常用的两种模式,主要用于控制锁的获取顺序,解决多线程环境中的资源竞争问题。

1. 公平锁 (Fair Lock)

定义:

  • 公平锁按照请求锁的顺序分配锁,遵循“先来先服务”原则。
  • 类似于排队系统,线程按等待时间顺序获取锁。

实现方式:

  • 在 Java 中,

    1
    ReentrantLock

    提供公平锁的实现:

    1
    2
    3
    4
    5
    java


    Copy code
    ReentrantLock lock = new ReentrantLock(true); // true 表示公平锁

优点:

  • 避免线程饥饿,确保每个线程最终都会获得锁。

缺点:

  • 性能较低,频繁切换线程带来更多开销。

2. 非公平锁 (Non-Fair Lock)

定义:

  • 非公平锁不保证按请求顺序分配锁,任何线程在尝试获取锁时都能直接竞争,可能插队成功。
  • 默认情况下,Java 中的 ReentrantLock 是非公平锁。

实现方式:

  • 不传递参数,默认构造即为非公平锁:

    1
    2
    3
    4
    5
    java


    Copy code
    ReentrantLock lock = new ReentrantLock(); // false 表示非公平锁

优点:

  • 性能更高,CPU 资源利用率更高,适用于高吞吐量场景。

缺点:

  • 存在线程饥饿的可能,某些线程可能长期无法获取锁。

3. 比较总结

特性 公平锁 非公平锁
获取顺序 按请求顺序获取(排队) 随机竞争,可能插队
实现方式 ReentrantLock(true) ReentrantLock(false)
优点 无线程饥饿,公平性高 性能更高,吞吐量更大
缺点 性能开销较大,切换频繁 可能导致线程饥饿
适用场景 需要严格按顺序执行的场景 高性能、高吞吐量场景
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
javaCopy codeimport java.util.concurrent.locks.ReentrantLock;

public class FairLockExample {
private static ReentrantLock fairLock = new ReentrantLock(true); // 公平锁

public static void main(String[] args) {
Runnable task = () -> {
for (int i = 0; i < 2; i++) {
try {
fairLock.lock();
System.out.println(Thread.currentThread().getName() + " 获取了锁");
} finally {
fairLock.unlock();
}
}
};

Thread t1 = new Thread(task, "线程1");
Thread t2 = new Thread(task, "线程2");
Thread t3 = new Thread(task, "线程3");

t1.start();
t2.start();
t3.start();
}
}

选择指南:

  • 需要严格顺序控制时(如银行排队系统),选用 公平锁
  • 追求高性能时(如高并发的缓存、任务调度),选用 非公平锁

问:独占锁与共享锁?

  1. ReentrantLock为独占锁(悲观加锁策略) 。
  2. ReentrantReadWriteLock中读锁为共享锁,写锁为独占锁。
  3. JDK1.8 邮戳锁(StampedLock), 不可重入锁读的过程中也允许获取写锁后写入。这样一来,我们读的数据就可能不一致,所以,需要一点额外的代码来判断读的过程中是否有写入,这种读锁是一种乐观锁, 乐观锁的并发效率更高,但一旦有小概率的写入导致读取的数据不一致,需要能检测出来,再读一遍就行。

在多线程环境中,锁用于控制对共享资源的访问,避免数据竞争和不一致问题。根据线程对锁的获取模式,锁可以分为 独占锁 (Exclusive Lock) 和 **共享锁 (Shared Lock)**。

1. 独占锁 (Exclusive Lock)

定义:

  • 独占锁一次只能被一个线程持有,其他线程必须等待锁释放后才能获取。

特点:

  • 线程独占资源,避免并发访问。
  • 常用于写操作,保证数据的一致性和完整性。

示例:

  • 在 Java 中,ReentrantLock 是典型的独占锁。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
javaCopy codeimport java.util.concurrent.locks.ReentrantLock;

public class ExclusiveLockExample {
private static ReentrantLock lock = new ReentrantLock();

public static void main(String[] args) {
Runnable task = () -> {
lock.lock(); // 获取独占锁
try {
System.out.println(Thread.currentThread().getName() + " 获取了独占锁");
Thread.sleep(2000); // 模拟执行任务
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 释放锁
System.out.println(Thread.currentThread().getName() + " 释放了独占锁");
}
};

new Thread(task, "线程1").start();
new Thread(task, "线程2").start();
}
}

2. 共享锁 (Shared Lock)

定义:

  • 共享锁可以被多个线程同时持有,允许多个线程同时读共享资源,但不允许写操作。

特点:

  • 适用于读操作,多个线程可以同时读取。
  • 写操作需要独占锁,避免数据不一致。

示例:

  • Java 中的

    1
    ReentrantReadWriteLock

    提供了共享锁的实现。

    • readLock() 是共享锁。
    • writeLock() 是独占锁。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
javaCopy codeimport java.util.concurrent.locks.ReentrantReadWriteLock;

public class SharedLockExample {
private static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private static String data = "共享数据";

public static void main(String[] args) {
Runnable readTask = () -> {
lock.readLock().lock(); // 获取共享锁
try {
System.out.println(Thread.currentThread().getName() + " 读取数据: " + data);
Thread.sleep(1000); // 模拟读取
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.readLock().unlock(); // 释放锁
}
};

Runnable writeTask = () -> {
lock.writeLock().lock(); // 获取独占锁
try {
data = Thread.currentThread().getName() + " 写入的数据";
System.out.println(Thread.currentThread().getName() + " 写入数据");
Thread.sleep(1000); // 模拟写入
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.writeLock().unlock(); // 释放锁
}
};

new Thread(readTask, "线程1").start();
new Thread(readTask, "线程2").start();
new Thread(writeTask, "线程3").start();
new Thread(readTask, "线程4").start();
}
}

3. 对比总结

特性 独占锁 (Exclusive Lock) 共享锁 (Shared Lock)
获取方式 仅允许一个线程持有 允许多个线程持有
适用场景 写操作,数据修改 读操作,数据读取
实现类 ReentrantLock ReentrantReadWriteLock.readLock()
线程安全性 高,确保数据完整性 允许并发读取,减少阻塞
性能表现 较低,因线程互斥阻塞 较高,允许多线程读共享资源

选择指南:

  • 读多写少场景: 使用共享锁,减少锁竞争,提高读性能。
  • 写多读少或频繁更新场景: 使用独占锁,确保数据完整性。
  • 混合场景: 使用 ReentrantReadWriteLock,根据操作类型选择合适的锁。

问:可重入锁概念?

  1. 可重入锁是指同一个线程可以多次获取同一把锁,不会因为之前已经获取过还没释放而阻塞;
  2. ReentrantLock、ReentrantReadWriteLock和synchronized都是可重入锁。
  3. 可重入锁的一个优点是可一定程度避免死锁。

可重入锁 是指同一个线程在持有锁的情况下,可以再次获取该锁而不会被阻塞。换句话说,线程可以“重入”到自己已经拥有的锁中,避免死锁问题。

1. 可重入锁的机制

实现原理:

  • 锁会记录持有该锁的线程重入的次数
  • 每次同一线程获取锁时,重入次数加一。
  • 每次释放锁时,重入次数减一,直到重入次数为零时,锁才真正释放。

2. 可重入锁的实现示例

示例 1: 使用 synchronized (隐式可重入锁)

synchronized 本质上是一个可重入锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
javaCopy codepublic class ReentrantLockExample {

public synchronized void methodA() {
System.out.println(Thread.currentThread().getName() + " 进入方法A");
methodB(); // 同一个线程可以重入方法B
}

public synchronized void methodB() {
System.out.println(Thread.currentThread().getName() + " 进入方法B");
}

public static void main(String[] args) {
ReentrantLockExample example = new ReentrantLockExample();
new Thread(example::methodA, "线程1").start();
}
}

运行结果:

1
2
cssCopy code线程1 进入方法A  
线程1 进入方法B

示例 2: 使用 ReentrantLock (显式可重入锁)

Java 提供了 ReentrantLock 类,明确实现了可重入锁机制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
javaCopy codeimport java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockTest {

private final ReentrantLock lock = new ReentrantLock();

public void methodA() {
lock.lock(); // 获取锁
try {
System.out.println(Thread.currentThread().getName() + " 进入方法A");
methodB(); // 重入锁
} finally {
lock.unlock(); // 释放锁
}
}

public void methodB() {
lock.lock(); // 获取锁
try {
System.out.println(Thread.currentThread().getName() + " 进入方法B");
} finally {
lock.unlock(); // 释放锁
}
}

public static void main(String[] args) {
ReentrantLockTest example = new ReentrantLockTest();
new Thread(example::methodA, "线程1").start();
}
}

运行结果:

1
2
cssCopy code线程1 进入方法A  
线程1 进入方法B

3. 可重入锁的特点

特性 解释
线程重入性 同一线程可以多次获取锁,不会死锁
计数维护 每次获取锁计数加一,释放锁计数减一
实现方式 synchronizedReentrantLock
锁粒度 方法级、代码块级
性能与灵活性 ReentrantLock 提供更灵活的锁操作,如尝试锁和定时锁

4. 可重入锁的应用场景

  • 递归调用: 在递归方法中,锁需要在同一线程内多次获取和释放。
  • 嵌套调用: 一个方法内部调用另一个加锁的方法时,避免死锁。
  • 复杂任务调度: 在并发框架中实现灵活的任务管理。

5. 可重入锁与非可重入锁的区别

特性 可重入锁 非可重入锁
线程获取锁 同一线程可以多次获取锁 同一线程再次获取会阻塞
锁实现类 ReentrantLock, synchronized 自定义实现等
死锁风险 存在潜在死锁风险
应用场景 常见于大多数 Java 锁机制 特定应用和自定义场景

结论:

  • 在 Java 中,synchronizedReentrantLock 都是可重入锁,适用于多线程环境。
  • 在实际应用中,优先考虑 synchronized,仅在需要更高灵活性时使用 ReentrantLock

问:讲讲4种锁状态?⭐⭐⭐

即重量级锁的四种状态,为了提高效率,尽量避免使用重量级锁:

  • 无锁:是否偏向-0,锁标志-01。

  • 偏向锁:是否偏向-1,锁标志-01。会偏向第一个访问锁的线程,当一个线程访问同步代码块获得锁时,会在对象头和栈帧记录里存储锁偏向的线程ID,当这个线程再次进入同步代码块时,就不需要CAS操作来加锁了,只要测试一下对象头里是否存储着指向当前线程的偏向锁,如果偏向锁未启动,new出的对象是普通对象(即无锁,有稍微竞争会成轻量级锁),如果启动,new出的对象是匿名偏向(偏向锁) 对象头主要包括两部分数据:Mark Word(标记字段, 存储对象自身的运行时数据)、class Pointer(类型指针, 是对象指向它的类元数据的指针)。

  • 轻量级锁(自旋锁) :锁标志-00。

    1. 在把线程进行阻塞操作之前先让线程自旋等待一段时间,可能在等待期间其他线程已经解锁,这时就无需再让线程执行阻塞操作,避免了用户态到内核态的切换。(自适应自旋时间为一个线程上下文切换的时间)

    2. 在用自旋锁时有可能造成死锁,当递归调用时有可能造成死锁

    3. 在 JDK 1.6 之后,JVM 进行了锁优化,让 synchronized 也能 利用自旋锁(轻量级锁),其中涉及 Lock Record

      synchronized 进入轻量级锁阶段时

      • 每个线程的栈帧(Stack Frame)中维护一个 Lock Record 结构。
      • 对象头的 Mark Word 指向 Lock Record
      • 自旋时,线程检查 Lock Record 指针是否指向自己,若不是,则继续自旋。
  • 重量级锁:锁标志-10。

线程进入同步代码块,先检查对象头的 Mark Word:

  • 如果 指向当前线程的 Lock Record,说明已获取锁(可重入)。
  • 如果 指向 null,则当前线程用 CAS 尝试将 Mark Word 指向自己的 Lock Record(自旋)。
  • 如果 CAS 失败(其他线程持有锁),则自旋一定次数后膨胀为重量级锁

释放锁

  • Lock RecordmarkOop 还原到对象头 Mark Word
  • 若有等待线程,则唤醒它们。

1. 无锁(Unlocked)

特点:

  • 对象在创建时默认是无锁状态。
  • 适用于单线程环境,无需加锁。

Mark Word 结构:

  • 存储对象的哈希码和分代年龄。

应用场景:

  • 单线程执行代码时。

2. 偏向锁(Biased Locking)

特点:

  • 偏向锁优化了无竞争的加锁场景。
  • 锁会“偏向”第一个获取它的线程,直到其他线程尝试获取该锁。
  • 如果没有竞争,持有锁的线程再次进入同步块无需重新加锁。

Mark Word 结构:

  • 记录持有锁的线程 ID。

触发升级:

  • 出现其他线程竞争时,锁会升级为轻量级锁。

应用场景:

  • 单线程执行的代码块,线程间锁竞争很少的场景。

3. 轻量级锁(Lightweight Locking)

特点:

  • 当多个线程竞争同一锁但没有真正的线程阻塞时,锁膨胀为轻量级锁。
  • 使用自旋锁(CAS)尝试获取锁。

Mark Word 结构:

  • 存储线程栈中锁记录的地址。

触发升级:

  • 竞争激烈时,自旋失败,锁升级为重量级锁。

应用场景:

  • 多线程环境中,锁竞争不激烈,适用于短时间执行的代码块。

4. 重量级锁(Heavyweight Locking)

特点:

  • 多线程竞争严重,轻量级锁无法满足要求时,锁升级为重量级锁。
  • 线程会被阻塞,等待锁释放,涉及内核调度。

Mark Word 结构:

  • 存储指向重量级锁的指针。

应用场景:

  • 锁竞争激烈,长时间执行的代码块。

锁状态演化流程图:

1
2
3
4
5
rust


Copy code
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁

锁状态切换时的注意点:

  • 锁只能从低级状态向高级状态升级,不能降级
  • JVM 默认开启偏向锁,可以通过 -XX:-UseBiasedLocking 禁用。

优化建议:

  1. 避免长时间持有锁,尽量减少锁的粒度。
  2. 使用并发包中的工具类(如 ReentrantLockConcurrentHashMap)。
  3. 在高竞争场景中,考虑 StampedLockReadWriteLock 提升并发性能。

问:轻量级锁与偏向锁的区别?⭐⭐

  1. 偏向锁对象头 Mark Word 中存放线程ID,轻量级锁则是栈帧中锁记录的指针。
  2. 偏向锁是在无竞争场景下完全消除同步,连CAS也不执行;轻量级锁是通过CAS来避免进入开销较大的互斥操作。
  3. 偏向锁遇到线程竞争会升级为轻量级锁,前者适用于只有一个线程访问同步块,后者则是线程竞争不激烈的场景。

1. 偏向锁(Biased Locking)

设计目标:

  • 优化 无竞争 场景,减少加锁和解锁的开销。

特点:

  • 偏向线程: 锁偏向于第一次获取它的线程,避免重复加锁。
  • 无竞争: 在没有其他线程竞争时,线程可以直接使用该锁。
  • 撤销成本: 当出现其他线程竞争时,需要撤销偏向锁,升级为轻量级锁。

加锁方式:

  • 使用 CAS 操作尝试将线程 ID 记录到锁的对象头(Mark Word)。

适用场景:

  • 单线程访问多次。

缺点:

  • 多线程场景频繁撤销锁时,性能下降。

2. 轻量级锁(Lightweight Locking)

设计目标:

  • 短时竞争 场景中避免线程阻塞,提高并发性能。

特点:

  • 竞争检测: 线程尝试通过自旋(CAS 操作)竞争锁。
  • 无阻塞: 没有线程阻塞,失败的线程自旋等待。
  • 升级路径: 如果竞争激烈,锁升级为重量级锁。

加锁方式:

  • 线程尝试通过 CAS 操作将锁记录拷贝到线程的栈帧中,更新对象头。

适用场景:

  • 多线程访问但锁竞争不激烈。

缺点:

  • 在高竞争环境中,自旋次数过多会导致性能下降,升级为重量级锁。

主要区别总结:

对比项 偏向锁 轻量级锁
设计目标 无竞争优化,减少加锁操作 短时间内少量竞争优化
适用场景 单线程多次访问 多线程少量竞争
加锁方式 将线程 ID 写入对象头 CAS 操作尝试锁记录到栈帧
撤销操作 存在撤销和升级为轻量级锁的开销 升级为重量级锁
性能表现 无竞争时性能极高 轻量级竞争时性能较好
失败处理 存在竞争时撤销锁状态 多次失败升级为重量级锁

选择策略:

  • 单线程场景:开启 偏向锁 提升性能。
  • 多线程少量竞争场景:轻量级锁 提供无阻塞性能。
  • 多线程高竞争场景:直接考虑更高级锁机制(如 重量级锁)。

问:自旋锁升级到重量级锁条件?⭐⭐

  1. 某线程自旋次数超过10次;
  2. 等待的自旋线程超过了系统core数的一半;

自旋锁在 Java 中是一种锁优化机制,适用于短时间内的锁竞争。自旋锁升级为重量级锁是为了应对竞争激烈的场景,避免 CPU 资源浪费。以下是自旋锁升级为重量级锁的主要条件:

升级条件

  1. 自旋失败次数超过阈值
    • JVM 默认的自旋次数由 -XX:PreBlockSpin 参数控制。
    • 如果一个线程在多次自旋后仍未获取到锁,JVM 判定当前竞争激烈,锁会升级为重量级锁,线程将进入 阻塞状态
  2. 多个线程竞争激烈
    • 当多个线程频繁竞争同一个锁,CAS 操作不断失败,自旋锁升级为重量级锁,竞争线程进入阻塞队列。
  3. 线程被挂起或中断
    • 如果持有锁的线程被挂起或中断,其他线程自旋失败,锁直接升级。
  4. 锁膨胀机制触发
    • 在高并发环境中,JVM 会自动调整锁的状态,锁从轻量级锁升级到重量级锁,使用操作系统的 内核同步机制(如监视器锁)

升级过程(锁膨胀过程)

  1. 初始状态:无锁(Lock-Free)
  2. 线程获取锁时:进入 偏向锁 状态,偏向第一个获取锁的线程。
  3. 若发生竞争:撤销 偏向锁,升级为 轻量级锁
  4. 自旋次数过多:轻量级锁失败,升级为 重量级锁

注意点

  • 自旋锁的优点: 避免线程阻塞和上下文切换的开销。
  • 重量级锁的优点: 在高竞争场景下更高效,避免了自旋锁的资源浪费。
  • 配置调整: 可以通过 JVM 参数调整锁策略,如 -XX:+UseBiasedLocking 控制偏向锁,-XX:PreBlockSpin 控制自旋次数。

适用场景对比

锁类型 适用场景 性能表现
自旋锁 短时间锁竞争 高性能,避免阻塞
重量级锁 高并发、竞争激烈场景 较低性能,支持阻塞

通过这种升级机制,Java 锁在不同的场景中动态调整,提供更好的性能和线程安全保障。

问:讲讲读写锁?优点?实现方式?

读写锁即分离了读锁和写锁,同一时刻允许多个线程访问,提高了读操作间的并发性。

优点有:

  • 保证了读操作间的并发。
  • 保证写操作对读操作的可见性。
  • 简化读写交互场景的编程方式。

并发包中提供了读写锁 ReentrantReanWriteLock ,和ReentrantLock相似,同样基于AQS,但是读写锁是基于共享资源的,不是互斥,关键在于state的处理,读写锁把高16为记为读状态,低16位记为写状态,从而分开了读写操作,读读情况其实就是读锁重入,读写/写读/写写都是互斥的,只要判断低16位就好了。读状态是所有线程获取读锁次数的总和,每个线程各自的获取次数则存在ThreadLocal中。

Java 提供了 ReentrantReadWriteLock 类,支持以下功能:

  1. 共享锁(读锁) - 允许多个线程同时持有。
  2. 独占锁(写锁) - 只能被一个线程持有,阻塞其他线程的读写操作。

关键方法

  • 读锁相关方法:
    • readLock().lock() - 获取读锁。
    • readLock().unlock() - 释放读锁。
  • 写锁相关方法:
    • writeLock().lock() - 获取写锁。
    • writeLock().unlock() - 释放写锁。

实现示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockExample {
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private int value = 0;

// 读操作
public int readValue() {
lock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " 读取值: " + value);
return value;
} finally {
lock.readLock().unlock();
}
}

// 写操作
public void writeValue(int newValue) {
lock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " 写入值: " + newValue);
value = newValue;
} finally {
lock.writeLock().unlock();
}
}
}

优点

  1. 提高并发度 - 多个线程可以同时读,提升读密集型任务的效率。
  2. 减少阻塞时间 - 写操作完成后,读取线程可以立即恢复。
  3. 线程安全 - 提供了内置的锁机制,避免手动同步。

适用场景

  • 缓存系统:多线程读取缓存,少数线程更新缓存。
  • 配置管理:频繁读取配置信息,偶尔更新配置。
  • 文件系统:读操作频繁,写操作较少。

注意事项

  1. 写锁优先级问题 - 默认策略是 写锁优先,避免读线程长时间占用导致写锁饥饿。
  2. 降级操作支持 - 支持从写锁降级为读锁,反之不行。
  3. 死锁风险 - 不正确使用可能导致死锁,需注意锁的获取与释放顺序。

通过读写锁,可以在读多写少的场景中 有效提高并发性能,合理配置和使用是保障系统稳定的重要手段。

问:乐观锁和悲观锁的区别?优缺点?使用场景?JDK中涉及到乐观锁和悲观锁的内容?这两种锁在Java和MySQL分别是怎么实现的?MySql的悲观锁怎么防止并发?⭐

乐观锁和悲观锁的区别:如字面意思,乐观锁指总是假设最好的情况,每次操作都不会上锁,而是更新时判断是否在此期间有被其他线程修改;悲观锁则相反,总假设最坏的情况,共享资源每次只给一个线程使用。

优缺点:两种锁适用于不同的场景,当共享资源竞争激烈时,乐观锁产生大量的更新失败,导致一些操作会浪费CPU资源如自旋;而资源竞争不那么激烈时,悲观锁所要进行的线程阻塞切换、等待唤醒等需要额外浪费CPU资源。

使用场景:乐观锁常见于多读少写的应用场景;悲观锁被应用于传统关系型数据库,如行锁,表锁,读锁,写锁等,适用于多写少读的场景。

悲观锁实现方式:

  • JDK:synchronizedRetreentLock 等。
  • MySQL:行锁,表锁,读锁,写锁等

乐观锁实现方式:(CAS,版本号机制)

  • JDK:CAS,JVM中的CAS操作通过处理器提供的 CMPXCHG 指令来实现原子操作。
  • MySQL:MVCC-多版本并发控制。

MySql的悲观锁怎么防止并发:TODO

乐观锁与悲观锁的区别

特性 乐观锁 (Optimistic Lock) 悲观锁 (Pessimistic Lock)
基本思想 假设冲突很少,操作前不加锁,仅在提交前验证。 假设冲突频繁,操作前立即加锁,防止并发。
实现方式 版本号机制、CAS 比较交换操作。 数据库锁机制或同步锁。
适用场景 读多写少,冲突概率低。 写多读少,冲突概率高。
性能 开销小,延迟低。 加锁和解锁成本较高,影响吞吐量。
失败处理 冲突时需要重试,存在自旋消耗。 等待或阻塞,易导致线程饥饿。

优缺点分析

乐观锁的优点与缺点

优点:

  • 高并发环境中性能更高。
  • 避免线程阻塞,节省系统资源。

缺点:

  • 存在重试机制,消耗 CPU 资源。
  • 不适合高冲突的场景。

悲观锁的优点与缺点

优点:

  • 提供更强的并发安全保障。
  • 避免了数据冲突带来的不一致问题。

缺点:

  • 锁的开销大,性能损耗明显。
  • 易导致死锁与线程饥饿。

Java 中的实现

1. 乐观锁(CAS机制)

  • 使用 java.util.concurrent.atomic 包中的类如 AtomicIntegerAtomicReference 等。
  • Unsafe.compareAndSwapInt() 实现了底层 CAS 操作。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
import java.util.concurrent.atomic.AtomicInteger;

public class OptimisticLockExample {
private AtomicInteger count = new AtomicInteger(0);

public void increment() {
int current;
do {
current = count.get();
} while (!count.compareAndSet(current, current + 1));
}
}

2. 悲观锁(同步机制)

  • 使用 synchronized 关键字或 ReentrantLock

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.util.concurrent.locks.ReentrantLock;

public class PessimisticLockExample {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();

public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}

MySQL 中的实现

1. 乐观锁实现

  • 使用 版本号机制时间戳

示例: 基于版本号的更新

1
2
3
4
5
6
7
8
9
10
11
12
sqlCopy code-- 数据表示例
CREATE TABLE inventory (
id INT PRIMARY KEY,
product_name VARCHAR(50),
stock INT,
version INT
);

-- 更新操作
UPDATE inventory
SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 5;

2. 悲观锁实现

  • 使用 锁机制,如 SELECT ... FOR UPDATE

示例: 基于悲观锁的更新

1
2
3
4
5
6
7
8
9
10
11
sqlCopy code-- 开启事务
START TRANSACTION;

-- 加悲观锁
SELECT stock FROM inventory WHERE id = 1 FOR UPDATE;

-- 执行库存更新
UPDATE inventory SET stock = stock - 1 WHERE id = 1;

-- 提交事务
COMMIT;

MySQL 中如何防止并发

  1. 事务隔离级别: 使用较高的事务隔离级别(如 REPEATABLE READSERIALIZABLE)。
  2. 锁机制: 使用行级锁(FOR UPDATE),防止数据竞争。
  3. 死锁检测: MySQL 内部死锁检测机制可主动回滚事务,避免死锁。
  4. 索引优化: 使用合适的索引,减少锁范围,提升性能。

通过灵活选择乐观锁与悲观锁,结合具体的应用场景和数据库特性,可以有效 提升系统的并发性能

问:什么是死锁?线上死锁如何处理?如何定位死锁?死锁产生的原因?如何预防避免?⭐⭐⭐

线上程序出现死锁,造成线程堆积,最终OOM。只能快速重启APP,定位到死锁问题后,发布修复补丁。

  1. 什么是死锁?
    • 并发场景下,线程因为相互等待对方资源释放,导致永久阻塞。
  2. 如何定位死锁?
    • jps打印PID,通过jstack PID查看死锁线程,生成JVM当前的线程快照,死锁一般是因为线程互相等待对方持有的对象。
    • 通过JConsole监控工具,检测线程死锁。
  3. 死锁产生的原因?会同时满足四个条件
    • 互斥条件:资源同时只能被一个线程占有,线程间互斥等待。
    • 请求和保持条件:线程请求的资源被其他线程占据,此线程被阻塞但不会释放已有的资源。
    • 不可剥夺条件:线程获得资源后不能被夺走,只能主动释放。
    • 循环等待条件:存在线程间的循环等待链路,前一个线程请求资源被下个线程持有。
  4. 如何处理/避免死锁?针对4个条件,破坏任意一个。一般是允许前三个必要条件,通过合理的分配算法确保不会形成封闭等待链。
    • 结合代码和业务情况,不使用互斥锁;比如使用原子操作、ThreadLocal、乐观锁等来保证线程安全。
    • 将所有线程共享资源管理起来,同时只给一个线程使用;一个线程一次只占用一个资源;牺牲性能,降低吞吐量。
    • 设置超时时间,让线程不能永久的持有资源。尝试使用定时锁,使用 lock.tryLock(timeout) 来代替内部锁机制。
    • 有序的获取资源,工作最快的线程不会遇到被锁住的资源。
  5. 如何预防死锁?
    • 银行家算法,一种资源分配和进程调度的算法,通过动态地分析系统中进程对资源的请求以及释放来判断是否分配资源,以避免可能导致死锁的资源分配情况。
    • 系统分配资源之前,通过预先检查资源分配是否会导致安全状态。
    • 优点是能够避免死锁,缺点是需要事先知道每个进程的最大需求,并且对系统的资源进行动态分析。

什么是死锁?

死锁 (Deadlock) 是指两个或多个线程(或进程)在执行过程中,由于相互持有并等待对方的资源,导致永远无法继续执行的状态。

死锁产生的四个必要条件

  1. 互斥条件: 至少有一个资源只能被一个线程持有。
  2. 占有且等待条件: 线程已持有资源,同时等待其他资源。
  3. 不可剥夺条件: 已获取的资源不能被强制剥夺,必须由线程主动释放。
  4. 循环等待条件: 存在一个线程等待循环链,每个线程都等待下一个线程持有的资源。

死锁的产生原因

  1. 资源竞争: 多线程争夺有限资源。
  2. 锁的嵌套: 多线程嵌套获取多个锁。
  3. 锁的获取顺序不一致: 不同线程按不同顺序获取锁。
  4. 线程优先级反转: 高优先级线程等待低优先级线程释放资源。

如何定位死锁?

1. 线程转储分析(Thread Dump)

  • 使用 jstack <PID> 命令获取 Java 应用程序的线程转储。
  • 查看线程状态,检查是否有 BLOCKED 状态和 Found one Java-level deadlock 提示。

2. 日志分析

  • 打印锁获取和释放的日志。
  • 使用日志分析工具(如 ELK)集中检索和过滤日志。

3. APM 工具监控

  • 使用应用性能监控 (APM) 工具,如 Prometheus、New Relic、Skywalking 等。

4. 数据库监控

  • 在数据库中使用死锁监控命令:

    1
    2
    3
    4
    5
    sql


    Copy code
    SHOW ENGINE INNODB STATUS;

**如何处理线上死锁? **

手动解决方案:

  1. 重启服务: 重启可能解除死锁,但应避免频繁重启。
  2. 线程中断: 使用 Thread.interrupt() 中断线程。
  3. 数据库回滚: 数据库死锁通常自动回滚。

自动化解决方案:

  1. 配置超时机制: 使用锁超时策略,避免无限等待。
  2. 负载分流: 将高并发请求分散到不同实例。
  3. 故障切换: 自动切换到备用服务实例。

如何预防和避免死锁?

代码层面

  1. 锁顺序一致: 确保所有线程按相同顺序获取锁。
  2. 减少锁持有时间: 尽快释放锁,减少锁的粒度。
  3. 尝试锁机制: 使用 tryLock() 设置锁超时,避免长时间等待。
  4. 避免嵌套锁: 避免嵌套锁,减少多锁依赖。
  5. 使用并发工具: 使用 java.util.concurrent 提供的高效并发工具,如 ReentrantLockSemaphoreCountDownLatch

数据库层面

  1. 优化事务: 尽量缩短事务时间,减少持有锁的时间。
  2. 减少锁争用: 合理设计数据库索引和分区,减少锁冲突。
  3. 显式加锁: 在必要时使用 SELECT ... FOR UPDATE

示例代码:避免死锁

正确使用 ReentrantLock 的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
javaCopy codeimport java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class DeadlockAvoidance {
private final Lock lock1 = new ReentrantLock();
private final Lock lock2 = new ReentrantLock();

public void operation1() {
if (lock1.tryLock()) {
try {
Thread.sleep(100); // 模拟操作
if (lock2.tryLock()) {
try {
System.out.println("Operation 1 completed");
} finally {
lock2.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock1.unlock();
}
}
}

public void operation2() {
if (lock2.tryLock()) {
try {
Thread.sleep(100); // 模拟操作
if (lock1.tryLock()) {
try {
System.out.println("Operation 2 completed");
} finally {
lock1.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock2.unlock();
}
}
}
}

通过 定位死锁分析原因采取预防措施,可以有效避免死锁,提高系统的稳定性和并发处理能力。

问:加锁会带来哪些性能问题。如何解决?

锁的开销来自于三部分:

  1. 上下文切换,挂起当前线程(存储当前上下文),恢复一个线程(找到一个合适的上下文并恢复到寄存器),跳转到程序计数器指向的位置(即线程被中断时的代码行)。
  2. 调度器开销,唤醒或休眠线程。
  3. 多核环境的跨处理器调度开销。

如何解决这些性能开销:

  1. 减少线程切换,如通过CAS尝试短时间内请求锁的自旋锁。
  2. 减少锁的冲突次数,比如读写锁分离,哈希表分段加锁等。
  3. 在较少竞争的环境下尽量避免使用锁,如 synchronized 对锁分等级,CAS + volatile 等

一、加锁的性能问题

  1. 线程阻塞与上下文切换成本
    • 原因: 线程获取锁失败时会阻塞,导致频繁的线程上下文切换。
    • 影响: 增加 CPU 使用率,降低应用程序吞吐量。
  2. 锁竞争(资源争用)
    • 原因: 多线程争夺同一个锁时,出现竞争,导致线程等待。
    • 影响: 高并发环境下,锁竞争严重,系统性能下降。
  3. 死锁风险
    • 原因: 多线程在不一致的锁获取顺序下,容易导致死锁。
    • 影响: 程序无法继续执行,需手动干预或重启服务。
  4. 缓存一致性问题
    • 原因: 多核 CPU 下的缓存同步机制(MESI 协议)会频繁刷新缓存,导致 CPU 性能下降。
    • 影响: 系统运行变慢,性能下降。
  5. 锁开销(锁的管理成本)
    • 原因: 加锁与解锁本身会引入额外的 CPU 开销。
    • 影响: 在锁持有时间短但频繁使用时,这一开销变得显著。

二、解决方案:优化加锁机制

1. 减少锁的粒度

  • 描述: 将大范围锁缩小到更精确的代码段或数据。

  • 示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    javaCopy code// 锁粒度过大
    synchronized (this) {
    criticalOperation();
    nonCriticalOperation();
    }

    // 优化后的代码
    criticalOperation();
    synchronized (this) {
    nonCriticalOperation();
    }

2. 使用读写锁(ReentrantReadWriteLock)

  • 描述: 允许多个线程并发读取,写操作独占,减少锁冲突。

  • 示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    javaCopy codeprivate final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public void read() {
    lock.readLock().lock();
    try {
    // 读操作
    } finally {
    lock.readLock().unlock();
    }
    }

    public void write() {
    lock.writeLock().lock();
    try {
    // 写操作
    } finally {
    lock.writeLock().unlock();
    }
    }

3. 使用无锁算法(CAS)

  • 描述: 使用 AtomicIntegerConcurrentHashMap 等基于 CAS 的无锁数据结构,避免锁开销。

  • 示例:

    1
    2
    3
    4
    5
    javaCopy codeprivate final AtomicInteger counter = new AtomicInteger(0);

    public void increment() {
    counter.incrementAndGet();
    }

4. 使用锁分段机制

  • 描述: 将锁划分为多个段,减少锁冲突。例如 ConcurrentHashMap

5. 使用线程局部变量(ThreadLocal)

  • 描述: 将数据与线程绑定,避免共享变量带来的锁竞争。

  • 示例:

    1
    2
    3
    4
    5
    java


    Copy code
    private final ThreadLocal<Integer> threadLocalValue = ThreadLocal.withInitial(() -> 0);

6. 尝试锁(TryLock)机制

  • 描述: 避免线程长时间等待锁,通过 tryLock() 设置超时机制。

  • 示例:

    1
    2
    3
    4
    5
    6
    7
    javaCopy codeif (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
    try {
    // 执行操作
    } finally {
    lock.unlock();
    }
    }

7. 锁消除(JIT 优化)

  • 描述: 编译时通过逃逸分析,自动消除不必要的锁。

三、其他优化策略

  1. 减少共享资源: 尽量减少共享数据,分离不同线程的数据。
  2. 优化并发策略: 选择适合的并发工具,如 ForkJoinPoolCompletableFuture 等。
  3. 数据库层优化: 优化事务范围和锁策略,如减少 SELECT ... FOR UPDATE 使用。
  4. 分布式锁优化: 在分布式系统中使用 Redis、Zookeeper 实现高效的分布式锁。

问:偏向锁、轻量级锁、自旋锁等优化?

Java 虚拟机通过多种锁优化策略来提升并发性能,主要包括 偏向锁轻量级锁自旋锁锁消除锁粗化

1. 偏向锁 (Biased Locking)

概念:

  • 偏向锁是一个优化,假设锁大多数情况下由同一个线程持有。
  • 第一次获得锁时,锁对象头记录当前线程 ID,以后该线程再进入同步块时,不需要再做锁竞争,直接进入。

优点:

  • 避免了线程 CAS 操作,提升性能。

触发条件:

  • 同一线程多次访问同步块时自动启用。

撤销条件:

  • 另一个线程尝试获取锁时,偏向锁会撤销,升级为轻量级锁。

适用场景:

  • 适用于锁竞争极少的单线程环境。

2. 轻量级锁 (Lightweight Locking)

概念:

  • 多线程争用锁时,轻量级锁通过 CAS 操作避免了重量级锁的阻塞。
  • 每个线程会将对象头的锁标记复制到自己创建的锁记录中,使用 CAS 尝试修改对象头的锁标记指向锁记录。

优点:

  • 避免线程阻塞,提高性能。

升级条件:

  • 如果 CAS 操作失败,表示有多个线程竞争,锁升级为重量级锁。

适用场景:

  • 适用于短时间锁竞争的场景。

3. 自旋锁 (Spin Lock)

概念:

  • 多线程竞争锁时,线程不会立即阻塞,而是循环等待一段时间。
  • 自旋期间线程不进入内核态,减少了线程阻塞与唤醒的开销。

优点:

  • 避免了线程的阻塞和唤醒,适合短期锁争用。

缺点:

  • 自旋消耗 CPU,等待时间过长可能导致性能下降。

优化策略:

  • JDK 默认开启自适应自旋锁,根据竞争历史动态调整自旋次数。

适用场景:

  • 适用于多核 CPU 和短期锁竞争环境。

4. 锁消除 (Lock Elimination)

概念:

  • 通过逃逸分析,在编译期消除不必要的同步操作。

示例:

1
2
3
4
5
6
javaCopy codepublic String concat(String a, String b) {
StringBuffer sb = new StringBuffer();
sb.append(a);
sb.append(b);
return sb.toString();
}
  • 编译期检测到 StringBuffer 没有逃逸出方法范围,锁会被消除。

适用场景:

  • 编译期静态分析时发现无线程竞争。

5. 锁粗化 (Lock Coarsening)

概念:

  • 编译器将多个连续的锁操作合并,减少锁的频繁获取与释放。

示例:

1
2
3
4
5
javaCopy codefor (int i = 0; i < 1000; i++) {
synchronized (lock) {
// 执行代码
}
}

优化为:

1
2
3
4
5
javaCopy codesynchronized (lock) {
for (int i = 0; i < 1000; i++) {
// 执行代码
}
}

适用场景:

  • 频繁获取与释放同一锁的场景。

锁状态的转换过程:

  1. 无锁状态: 初始状态,无线程竞争。
  2. 偏向锁: 单个线程多次使用时自动启用。
  3. 轻量级锁: 多个线程尝试获取锁时,通过 CAS 进入轻量级锁。
  4. 重量级锁: 多线程锁竞争激烈,升级为重量级锁,线程阻塞。

应用场景总结:

锁类型 优点 缺点 适用场景
偏向锁 无竞争时性能最佳 有竞争时撤销成本高 无竞争,单线程环境
轻量级锁 无阻塞,减少线程切换 多线程时性能下降 短时锁竞争
自旋锁 避免阻塞,提高吞吐量 消耗 CPU,等待过长损耗 短时间高频竞争
重量级锁 保证数据一致性 阻塞线程,切换成本高 高强度并发竞争
锁消除 消除不必要的锁 仅限编译期优化 无跨线程资源访问
锁粗化 减少锁频繁获取与释放 需编译器自动优化 持续锁操作场景

问:事务有哪些特性?怎么理解原子性?

数据库事务具有四个关键特性,称为 ACID,分别是:

  1. 原子性 (Atomicity)
  2. 一致性 (Consistency)
  3. 隔离性 (Isolation)
  4. 持久性 (Durability)

1. 原子性 (Atomicity)

定义:

  • 原子性指事务中的操作要么全部成功执行,要么全部回滚,事务不可分割。

理解:

  • 事务内的多个数据库操作被视为一个整体。如果在执行过程中出现任何错误,数据库会回滚到事务开始之前的状态,确保操作不会有部分成功部分失败的情况。

示例:

假设有一个银行转账操作:

1
2
3
4
5
text


Copy code
账户 A 转账 100 元到账户 B

这包含两个操作:

  • 从账户 A 扣除 100 元
  • 向账户 B 增加 100 元

如果在执行第一个操作后发生系统崩溃,第二个操作未执行,系统需要回滚,保证账户 A 的余额恢复到转账前的状态,防止资金丢失。

2. 一致性 (Consistency)

定义:

  • 事务执行前后,数据库必须保持一致状态,满足数据完整性约束规则。

理解:

  • 数据库中的约束(如主键、外键、唯一性等)在事务执行后必须仍然有效,确保数据不会出现非法状态。

示例:

  • 银行系统中,转账过程中,两个账户的总余额必须保持不变。即使在事务中断时,也必须恢复到原始状态或最终正确的状态。

3. 隔离性 (Isolation)

定义:

  • 多个事务并发执行时,各事务之间相互独立,不能相互影响。

理解:

  • 一个事务的中间状态对其他事务是不可见的,事务隔离通过锁机制等手段来实现。

示例:

  • 两个用户同时购买库存为 1 的商品。事务隔离确保只有一个用户能成功下单,另一个用户会被阻塞或看到更新后的库存。

隔离级别(从低到高):

  • READ UNCOMMITTED: 允许读取未提交的数据(脏读)。
  • READ COMMITTED: 只允许读取已提交的数据(避免脏读)。
  • REPEATABLE READ: 多次读取数据结果相同(避免不可重复读)。
  • SERIALIZABLE: 完全隔离,事务串行执行(避免幻读)。

4. 持久性 (Durability)

定义:

  • 事务一旦提交,修改结果必须永久保存,即使发生系统故障或崩溃。

理解:

  • 提交后的数据必须存储在持久性存储设备中,如磁盘或数据库日志文件,确保事务结果不会丢失。

示例:

  • 在电商系统中,用户下单后收到订单确认,即使系统随后崩溃,用户的订单数据也应保存在数据库中。
属性 定义 示例
原子性 全部成功或全部回滚 转账时两账户同时增减金额
一致性 数据在事务前后必须满足完整性规则 转账后两账户的总余额不变
隔离性 多事务并发执行时互不干扰 两用户同时购买同一库存商品
持久性 提交的数据永久保存 下单成功后即使系统崩溃订单仍在

2.2 volatile

问:volatile的作用是什么?可见性?⭐⭐⭐

volatile变量
(1):变量可见性

(2):防止指令重排序

(3):保障变量单次读,写操作的原子性,但不能保证i++这种操作的原子性,因为本质是读,写两次操作

在 Java 中,volatile 是一种轻量级的同步机制,主要用来解决 可见性禁止指令重排序 问题,确保变量在多线程环境中的正确性。

volatile 的两个核心功能

  1. 保证可见性 (Visibility):

    • 当一个线程修改了 volatile 修饰的变量,其他线程能够立即看到最新值。

    • JVM 会强制将变量的新值从线程工作内存刷新到主内存。

    • 示例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      javaCopy codeclass VolatileExample {
      private volatile boolean running = true;

      public void stop() {
      running = false; // 修改了 running,其他线程可见
      }

      public void run() {
      while (running) {
      // 如果没有 volatile,可能永远不会退出循环
      }
      }
      }
  2. 禁止指令重排序 (Orderliness):

    • volatile 禁止 JVM 对该变量的读写操作进行指令重排序。

    • 这在 双重检查锁定单例模式 中非常重要,避免在初始化过程中发生对象未完全初始化的问题。

    • 示例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      javaCopy codeclass Singleton {
      private static volatile Singleton instance;

      public static Singleton getInstance() {
      if (instance == null) {
      synchronized (Singleton.class) {
      if (instance == null) {
      instance = new Singleton(); // 禁止重排序,确保初始化安全
      }
      }
      }
      return instance;
      }
      }

为什么需要可见性?

在 Java 内存模型中,每个线程都有自己的 **工作内存 (Working Memory)**,从主内存中拷贝变量的值进行操作。如果没有 volatile 修饰,线程可能只操作工作内存中的值,而不更新主内存,导致数据不同步的问题。

注意事项与局限性

  1. 不保证原子性:
    • volatile 不能保证复合操作的原子性,如自增 count++
    • 解决方法:使用 AtomicInteger 或显式锁 synchronized
  2. 适用场景:
    • 状态标志变量(如 boolean running)。
    • 不涉及复合操作的变量(如单一赋值)。

小结:volatile 的核心作用

功能 描述
可见性 (Visibility) 保证变量的修改对所有线程立即可见
禁止重排序 (Order) 避免指令重排序,确保内存可见性顺序
不保证原子性 需配合 synchronizedAtomic
适用场景 状态标志、简单变量同步等轻量场景

volatile 是多线程开发中的关键修饰符,在保证变量可见性和内存顺序上非常重要,但需结合实际业务场景,避免误用带来的性能问题。

问:volatile 的实现原理?及内存屏障相关?⭐⭐

volatile 的实现依赖于 内存屏障 (Memory Barrier) 和 **缓存一致性协议 (Cache Coherence Protocol)**。这些机制确保 volatile 变量在多线程环境中的可见性与有序性。

1. 内存屏障 (Memory Barrier)

内存屏障 是一条 CPU 指令,强制处理器在读写特定变量时执行特定操作,从而避免重排序和保证内存可见性。

内存屏障的作用:

  • 强制刷新缓存: 确保当前线程的缓存写入主内存。
  • 禁止指令重排序: 阻止处理器对内存屏障前后的指令进行重排序。

内存屏障类型:

  1. LoadLoad 屏障:
    • 作用:禁止读操作重排序。
    • 示例:Load1; LoadLoad; Load2 确保 Load1 必须在 Load2 之前执行。
  2. StoreStore 屏障:
    • 作用:禁止写操作重排序。
    • 示例:Store1; StoreStore; Store2 确保 Store1 必须在 Store2 之前执行。
  3. LoadStore 屏障:
    • 作用:禁止读操作与后续写操作重排序。
    • 示例:Load1; LoadStore; Store2 确保 Load1Store2 之前执行。
  4. StoreLoad 屏障:
    • 作用:禁止写操作与后续读操作重排序。
    • 示例:Store1; StoreLoad; Load2 确保 Store1Load2 之前完成。
    • 最强屏障,影响最大,常用于 volatile 实现。

2. volatile 的内存屏障实现机制

根据 Java 内存模型(JMM),volatile 变量的读写会插入内存屏障,具体规则如下:

  • 写入 volatile 变量时:
    • 插入 StoreStoreStoreLoad 屏障,确保写操作完成,刷新到主内存。
    • 确保后续读写操作不会被重排序到前面。
  • 读取 volatile 变量时:
    • 插入 LoadLoadLoadStore 屏障,确保变量从主内存读取,避免读取陈旧数据。
    • 确保读取操作之前的写操作不会被重排序。

3. 缓存一致性协议(MESI 协议)

为了多核 CPU 环境中数据一致性,Java 使用 CPU 的 缓存一致性协议 (如 MESI),确保多个 CPU 核心对共享变量的操作是同步的。

  • 原理:
    • 当一个线程修改 volatile 变量时,CPU 会通过总线通知其他 CPU 该变量失效,强制其他 CPU 从主内存重新加载该变量。

示例分析:volatile 实现内存屏障示意图

1
2
3
4
5
6
7
8
9
10
11
javaCopy codeclass VolatileExample {
private volatile int count = 0;

public void update() {
count = 10; // 写操作,插入 StoreStore + StoreLoad
}

public int read() {
return count; // 读操作,插入 LoadLoad + LoadStore
}
}

小结:volatile 实现原理总结

机制 功能
内存屏障 (Memory Barrier) 禁止重排序,确保内存可见性
缓存一致性协议 (MESI) 保证多核 CPU 间数据一致性
禁止重排序规则 读操作插入 LoadLoadLoadStore
写操作插入 StoreStoreStoreLoad

通过内存屏障与缓存一致性协议,volatile 实现了对变量的 可见性有序性,确保多线程环境中的数据正确性和一致性。

问:volatile如何保证线程间可见和避免指令重排?

volatile可见性是有指令原子性保证的,在jmm中定义了8类原子性指令,比如write,store,read,load。而volatile就要求write-store,load-read成为一个原子性操作,这样子可以确保在读取的时候都是从主内存读入,写入的时候会同步到主内存中(准确来说也是内存屏障),指令重排则是由内存屏障来保证的,由两个内存屏障:

  • 一个是编译器屏障:阻止编译器重排,保证编译程序时在优化屏障之前的指令不会在优化屏障之后执行。
  • 第二个是cpu屏障:sfence保证写入,lfence保证读取,lock类似于锁的方式。java多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个lock指令,就是增加一个完全的内存屏障指令。

volatile 是 Java 内存模型 (JMM) 中用于多线程环境的轻量级同步机制。它通过 可见性有序性(防止指令重排) 两个特性来确保数据的正确性。

1. 保证线程间的可见性

机制:内存屏障与缓存一致性协议

  • 内存屏障(Memory Barrier):
    • volatile 变量的写操作会在写入主内存时插入内存屏障,确保更改立即被其他线程可见。
    • volatile 变量的读操作会从主内存中重新读取,防止读取过期数据。
  • 缓存一致性协议(如 MESI 协议):
    • 多核 CPU 使用 MESI 协议,当一个线程修改了 volatile 变量,CPU 会通过总线协议使其他 CPU 的缓存行失效,强制其他线程从主内存重新加载该变量。

示例:可见性问题的修复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
javaCopy codeclass VisibilityExample {
private volatile boolean running = true;

public void start() {
new Thread(() -> {
while (running) {
// busy-waiting
}
System.out.println("Stopped");
}).start();
}

public void stop() {
running = false; // 写入操作立即可见
}
}

解释:

  • 当主线程调用 stop(),修改了 running 的值。
  • 因为 running 被声明为 volatile,它的更新会立即刷新到主内存。
  • 子线程每次从主内存读取 running,所以它会看到最新值,正常退出循环。

2. 避免指令重排(有序性)

机制:内存屏障规则与指令重排优化限制

Java 内存模型会在 volatile 变量的读写操作前后插入适当的内存屏障,确保代码在执行时保持特定的顺序:

重排序规则:

  1. volatile 变量时:
    插入 StoreStoreStoreLoad 屏障:
    • StoreStore: 确保写操作前的普通写操作不会与 volatile 写操作重排。
    • StoreLoad: 禁止写操作与后续的读操作重排。
  2. volatile 变量时:
    插入 LoadLoadLoadStore 屏障:
    • LoadLoad: 确保前面的读取操作不会与当前 volatile 读操作重排。
    • LoadStore: 确保前面的读取操作不会与后续写操作重排。

示例:禁止指令重排(双重检查锁实现单例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
javaCopy codeclass 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;
}
}

解释:

  • volatile 关键字确保 instance 的赋值操作不会被重排序。
  • 在没有 volatile 时,可能出现重排序:对象内存分配完成,但构造方法未执行,其他线程却访问了未初始化的对象。

小结:volatile 的两大特性与实现机制

功能 实现机制
线程间可见性 主内存同步 + 缓存一致性协议
防止指令重排 内存屏障规则(读/写屏障插入)

通过这两个机制,volatile 保证了线程间数据共享的可见性,并防止指令重排,提高了多线程环境下的数据一致性与代码执行的有序性。


2.3 synchronized

问:synchronized 使用方式及实现原理,以及锁优化?⭐⭐⭐

synchronized 是 Java 中用于实现线程同步的关键字,它通过 监视器锁(Monitor) 来保证同一时间只有一个线程可以执行被保护的代码块或方法。以下是 synchronized 的实现原理和相关概念的详细解释:

  1. synchronized 的基本原理

    • 监视器锁(Monitor):记录了持有锁的线程和锁的重入次数**。

      • 每个 Java 对象都有一个与之关联的监视器锁(Monitor)。
      • 当线程进入 synchronized 代码块时,会尝试获取对象的监视器锁。
      • 如果锁被其他线程持有,当前线程会进入阻塞状态,直到锁被释放。
    • monitorentermonitorexit 指令

      • synchronized 的底层实现依赖于 JVM 的一对字节码指令:

        • monitorenter:尝试获取对象的监视器锁。
        • monitorexit:释放对象的监视器锁。
      • 这两个指令确保同一时间只有一个线程可以执行 synchronized 代码块。

        1
        2
        3
        4
        5
        6
        7
        8
        9
        public void synchronizedMethod() {
        synchronized (this) {
        // 同步代码块
        }
        }
        对应的字节码:
        monitorenter
        // 同步代码块
        monitorexit
      • 获取锁的过程:线程执行 monitorenter 指令,尝试获取对象的监视器锁 Monitor

        • 如果 Monitor 为空(未被持有)
          • 线程 **获取锁,设置 owner**。
          • **重入计数器 recursion = 1**。
        • 如果 Monitor 被当前线程持有:允许重入recursion++
        • 如果 Monitor 被其他线程持有
          • 线程进入 ContentionList,进行自旋等待
          • 如果自旋失败,则进入阻塞
      • 释放锁的过程:**当前线程调用 monitorexit**指令,释放锁并将进入数减 1。

        • recursion--,如果 recursion == 0,锁被释放。
        • entryListContentionList 选一个线程作为 onDeck 进入竞争。
  2. synchronized 的四种状态:

    • 无锁状态:对象的监视器锁未被任何线程持有。
    • 偏向锁:如果只有一个线程访问锁,JVM 会将锁偏向该线程,避免重复的同步操作。
    • 轻量级锁:当多个线程竞争锁时,JVM 会将锁升级为轻量级锁,使用 CAS 操作来竞争锁。
    • 重量级锁:当竞争激烈时,JVM 会将锁升级为重量级锁,线程会进入阻塞状态,等待锁的释放。

    锁升级流程:

    1. 无锁 → 偏向锁:第一次访问 synchronized 代码块时,给对象打上偏向标记。通过 CAS 操作将线程 ID 记录在对象头中。
    2. 偏向锁 → 轻量级锁:如果其他线程尝试获取锁,撤销偏向锁,升级为轻量级锁。使用 CAS 操作竞争锁。
    3. 轻量级锁 → 重量级锁:如果多个线程竞争失败,则进入阻塞队列,升级为重量级锁。线程会进入阻塞状态,等待锁的释放。
  3. 几个关键队列:

    名称 作用
    ContentionList(竞争队列) 竞争锁的线程队列(自旋线程会加入)所有请求锁的线程会首先进入 ContentionList。这是一个虚拟队列,线程会通过 CAS 操作自旋竞争锁。
    EntryList(候选队列) 有资格的候选者队列(轻量级锁竞争失败,进入 EntryList)当锁被释放时,JVM 会从 ContentionList 中选择一些线程进入 EntryList。EntryList 中的线程有资格竞争锁。
    WaitSet(等待队列) 调用 wait() 进入的阻塞队列(必须 notify() 唤醒)当线程调用 wait() 方法时,会释放锁并进入 WaitSet。当其他线程调用 notify()notifyAll() 时,WaitSet 中的线程会被唤醒并重新进入 EntryList。
    onDeck(竞争候选者) 当前的竞争候选者;从 EntryList 中选择一个线程作为竞争候选者(OnDeck)。OnDeck 线程会尝试获取锁。
    owner(锁持有者) 当前持有锁的线程;成功获取锁的线程成为 Owner。Owner 线程执行完同步代码块后,会释放锁并唤醒其他线程。
    !owner 锁已释放,等待 onDeck 线程竞争
  4. synchronized的非公平性:

    • 自旋抢占
      • 新请求锁的线程会先尝试自旋获取锁,如果成功则直接抢占锁。
      • 这会导致已经在 ContentionList 中等待的线程无法公平地获取锁。
    • 直接抢占
      • 自旋获取锁的线程可能会直接抢占 OnDeck 线程的锁资源。

多线程并发 之 synchronized 锁的优化

问:synchronized 在静态方法和普通方法的区别?

1. 锁对象的区别

修饰方法类型 锁对象 影响范围
普通方法(实例方法) 当前实例 (this) 同一个实例对象
静态方法(类方法) 当前类对象 (Class<?>) 整个类(包括所有实例)

示例代码:

1
2
3
4
5
6
7
8
9
10
11
class Example {
// 锁的是当前实例对象
public synchronized void instanceMethod() {
System.out.println("实例方法执行");
}

// 锁的是类对象
public static synchronized void staticMethod() {
System.out.println("静态方法执行");
}
}

2. 多线程环境下的锁行为

普通方法(实例方法)示例:

1
2
3
4
5
Example obj1 = new Example();
Example obj2 = new Example();

new Thread(() -> obj1.instanceMethod()).start(); // 锁 obj1
new Thread(() -> obj2.instanceMethod()).start(); // 锁 obj2

解释:

  • 两个线程锁定不同实例 obj1obj2,因此不会互相阻塞。

静态方法(类方法)示例:

1
2
3
4
5
Example obj1 = new Example();
Example obj2 = new Example();

new Thread(() -> obj1.staticMethod()).start(); // 锁 Example.class
new Thread(() -> obj2.staticMethod()).start(); // 仍锁 Example.class

解释:

  • 不论通过哪个实例调用静态方法,锁的都是类对象 Example.class,两个线程会互相阻塞。

3. 并发控制的应用场景

普通方法的应用场景:

适用于对 实例级别资源 的同步,如处理某个对象的属性。

静态方法的应用场景:

适用于对 类级别资源 的同步,如全局缓存、配置信息等。

4. 示例对比演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Demo {
public synchronized void instanceMethod() {
System.out.println(Thread.currentThread().getName() + " - 实例方法开始");
try { Thread.sleep(1000); } catch (InterruptedException e) { }
System.out.println(Thread.currentThread().getName() + " - 实例方法结束");
}

public static synchronized void staticMethod() {
System.out.println(Thread.currentThread().getName() + " - 静态方法开始");
try { Thread.sleep(1000); } catch (InterruptedException e) { }
System.out.println(Thread.currentThread().getName() + " - 静态方法结束");
}
}

public class Main {
public static void main(String[] args) {
Demo obj1 = new Demo();
Demo obj2 = new Demo();

new Thread(() -> obj1.instanceMethod(), "线程1").start();
new Thread(() -> obj2.instanceMethod(), "线程2").start();

new Thread(() -> Demo.staticMethod(), "线程3").start();
new Thread(() -> Demo.staticMethod(), "线程4").start();
}
}

5. 结论

比较点 普通方法 (synchronized) 静态方法 (synchronized)
锁的范围 当前实例对象 this 当前类对象 Class<?>
多实例的执行关系 不互相影响 互相阻塞
同一实例的多线程 互斥 互斥
使用场景 实例级别资源同步 类级别资源同步

通过选择合适的锁类型,可以有效地控制线程间的同步,避免资源竞争和数据不一致问题。

问:synchronized 和 ReentranLock的区别?⭐⭐

  • 都是可重入锁; R是显示获取和释放锁,s是隐式;
  • R更灵活可以知道有没有成功获取锁,可以定义读写锁,是api级别,s是JVM级别;
  • R可以定义公平锁;Lock是接口,s是java中的关键字

synchronizedJVM 层面的关键字,基于 Monitor

LockJDK 提供的类,基于 AQS,更灵活。

synchronized 非公平ReentrantLock 支持公平锁

Lock 支持 tryLock() 超时获取锁可中断

ReentrantLock 为什么比 synchronized 性能更好?

回答要点:

  • synchronized 依赖 JVM monitor,线程阻塞时依赖 OS 调度(重量级锁)。
  • ReentrantLock 通过 CAS 和 AQS 进行高效排队,避免线程频繁挂起 / 唤醒。
对比项 synchronized ReentrantLock
实现原理 基于对象 Monitor 基于 AbstractQueuedSynchronizer (AQS)
锁类型 非公平锁 支持公平 & 非公平
锁状态 支持锁升级 独占锁 / 共享锁
阻塞方式 依赖 OS 线程调度 CAS + AQS 队列管理
可中断性 不可中断 支持 lockInterruptibly()
tryLock() 不支持 支持 tryLock() 超时获取
条件等待 Object.wait()notify()notifyAll() 实现条件等待 使用 Condition 对象,可以绑定多个条件
适用场景 同步代码块,简单场景 高并发 + 灵活锁控制

问:HashTable,同步锁,synchronized 关键字 1.6 之后提升了什么,怎么提升的这些?

1. Hashtable 中的同步机制

  • Hashtable 是线程安全的,它通过 synchronized 关键字对方法进行加锁,保证多线程环境下的数据一致性。
  • 示例:
1
2
3
4
5
6
7
8
9
10
11
javaCopy codepublic synchronized V get(Object key) {
// 加锁,确保线程安全
Entry<?,?> tab[] = table;
int hash = key.hashCode();
for (Entry<?,?> e = tab[(hash & 0x7FFFFFFF) % tab.length]; e != null; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
return null;
}

问题:Hashtable 的同步性能瓶颈

  • 每个方法都使用 synchronized,锁粒度大,竞争激烈时性能下降。
  • 锁的范围涵盖了整个对象,导致线程争用严重。

2. synchronized 在 Java 1.6 之后的优化

Java 1.6 对 synchronized 进行了多项优化,提升了其性能,以下是主要的改进:

(1) 偏向锁 (Biased Locking)

  • 作用: 消除不必要的加锁操作,优化无竞争场景。
  • 机制: 当一个线程第一次获取锁时,JVM 将锁标记为“偏向该线程”。若该线程再次获取锁,无需进行同步操作。
  • 适用场景: 只有一个线程访问同步块时,性能大幅提升。

(2) 轻量级锁 (Lightweight Locking)

  • 作用: 在竞争不激烈时,减少加锁和解锁的开销。
  • 机制: 在锁对象的对象头中保存锁信息,通过 CAS 操作尝试获取锁,失败时升级为重量级锁。
  • 适用场景: 少量线程争用时。

(3) 自旋锁 (Spin Lock)

  • 作用: 避免线程在锁争用时频繁挂起与恢复。
  • 机制: 在锁被占用时,线程进行有限次数的忙等待,而不是直接进入阻塞状态。
  • 适用场景: 锁竞争时间短时,减少线程切换开销。

(4) 锁消除 (Lock Elimination)

  • 作用: 在编译期间,JVM 自动消除不必要的锁。
  • 机制: JVM 使用逃逸分析,判断对象的作用域,如果发现对象不会被其他线程访问,自动移除 synchronized 锁。
  • 适用场景: 局部对象只在当前线程使用时。

(5) 锁粗化 (Lock Coarsening)

  • 作用: 降低频繁加锁与解锁的开销。
  • 机制: JVM 自动扩大锁的作用范围,将多个连续的同步块合并,减少锁的获取与释放次数。
  • 适用场景: 多个连续的同步操作对同一对象进行加锁。

(6) 重量级锁 (Heavyweight Locking)

  • 作用: 在锁竞争激烈时,升级为重量级锁。
  • 机制: 使用操作系统的 Monitor 对象,线程直接阻塞,等待唤醒。

优化示例:对比前后性能

未优化前:

1
2
3
javaCopy codepublic synchronized void method() {
System.out.println("同步方法执行...");
}

优化后:

1
2
3
4
5
javaCopy codepublic void method() {
synchronized(this) {
System.out.println("同步代码块执行...");
}
}

3. Hashtable 的替代方案:ConcurrentHashMap

  • 为了提高性能,Java 提供了

    1
    ConcurrentHashMap

    ,它采用

    分段锁(Segmented Lock)

    技术,提升了并发性能:

    • 分为多个段,每个段有独立的锁,减少锁竞争。
    • 采用非阻塞算法,如 CAS,进一步优化。

4. 结论:提升的核心要点

  • 锁优化概述: 偏向锁 → 轻量级锁 → 自旋锁 → 重量级锁(逐步升级,减少锁开销)。
  • 性能改进原因:
    • 锁竞争减少
    • 加锁解锁操作优化
    • 使用更高效的数据结构与算法(如 CAS)。

这些改进使 synchronized 的性能在 Java 1.6 及其之后版本得到了显著提升,减少了早期 Java 版本中对 ReentrantLock 的强依赖。


2.4 Lock

问:Lock 接口有哪些实现类,使用场景是什么?

Lock 接口是 Java 并发框架中用于实现线程同步的核心接口,提供了更灵活的锁机制。以下是常见的 Lock 实现类及其适用场景:

1. ReentrantLock (重入锁)

特性:

  • 可重入:线程可以多次获取同一个锁。
  • 支持公平锁和非公平锁(默认)。
  • 提供条件变量 (Condition)。
  • 支持中断和尝试获取锁 (tryLock)。

适用场景:

  • 锁需要显式控制的场景。
  • 高度竞争的资源共享,确保公平性。
  • 需要精确控制锁释放与获取的顺序。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
javaCopy codeimport java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();

public void safeMethod() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 正在执行任务");
} finally {
lock.unlock();
}
}
}

2. ReentrantReadWriteLock (读写锁)

特性:

  • 支持多个读线程并发,写线程独占。
  • 提供两种锁:
    • ReadLock(读锁):共享锁,多个线程可同时读。
    • WriteLock(写锁):独占锁,只有一个线程可写。

适用场景:

  • 读多写少的场景,例如缓存、配置信息读取。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
javaCopy codeimport java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockExample {
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private int data;

public void readData() {
lock.readLock().lock();
try {
System.out.println("读取数据:" + data);
} finally {
lock.readLock().unlock();
}
}

public void writeData(int newData) {
lock.writeLock().lock();
try {
data = newData;
System.out.println("写入数据:" + data);
} finally {
lock.writeLock().unlock();
}
}
}

3. StampedLock (标记锁)

特性:

  • 提供三种锁模式:
    • 写锁(独占)
    • 悲观读锁(共享)
    • 乐观读锁(无锁)

适用场景:

  • 高并发下,读操作远多于写操作,且读操作允许一定程度的数据不一致。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
javaCopy codeimport java.util.concurrent.locks.StampedLock;

public class StampedLockExample {
private final StampedLock lock = new StampedLock();
private int value;

public int optimisticRead() {
long stamp = lock.tryOptimisticRead();
int result = value;
if (!lock.validate(stamp)) {
stamp = lock.readLock();
try {
result = value;
} finally {
lock.unlockRead(stamp);
}
}
return result;
}

public void writeValue(int newValue) {
long stamp = lock.writeLock();
try {
value = newValue;
} finally {
lock.unlockWrite(stamp);
}
}
}

4. Condition (条件锁)

特性:

  • ReentrantLock 一起使用。
  • 提供类似 Object.wait()Object.notify() 的功能。
  • 支持精确的等待和唤醒机制。

适用场景:

  • 需要多个条件变量时。
  • 复杂的等待与唤醒逻辑,例如生产者-消费者模型。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
javaCopy codeimport java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionExample {
private final ReentrantLock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private boolean ready = false;

public void produce() {
lock.lock();
try {
ready = true;
System.out.println("生产完成,通知消费者");
condition.signal();
} finally {
lock.unlock();
}
}

public void consume() {
lock.lock();
try {
while (!ready) {
System.out.println("等待生产...");
condition.await();
}
System.out.println("消费完成");
ready = false;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}
}

选择指南:何时使用哪种锁?

实现类 适用场景 关键特性
ReentrantLock 精确控制锁操作、需要条件变量 可重入、公平/非公平锁
ReentrantReadWriteLock 读多写少、缓存、配置管理 独占写锁,共享读锁
StampedLock 读操作远多于写操作,数据一致性可放宽 乐观读,悲观读,写锁
Condition 复杂的等待与唤醒,精确通知 ReentrantLock 配合使用

总结:选择锁的原则

  1. 高并发读多写少场景: 使用 ReentrantReadWriteLockStampedLock
  2. 精确控制锁释放和获取: 使用 ReentrantLock
  3. 等待与唤醒机制: 使用 Condition
  4. 性能与灵活性兼顾: 优先考虑 StampedLock,对乐观读的支持性能更好。

根据实际需求选择合适的锁实现,可以在并发编程中有效提高程序的性能和可扩展性。

问:讲讲ReentrantLock实现原理?⭐⭐⭐

ReentrantLock原理:

  1. 基于 CAS(Compare-And-Swap)AQS(AbstractQueuedSynchronizer) 来实现锁的获取和释放。

    • CAS 是一种原子操作,用于实现无锁的线程安全操作。在 ReentrantLock 中,CAS 用于尝试获取锁的状态(如将锁的状态从 0 改为 1)。

    • CAS 只能保证单次加锁,但如果失败,需要排队等待,当有多个线程争用同一把锁时,必须有类似排队的机制去将那些没能拿到锁的线程串联在一起,避免一直自旋浪费CPU。这就是 AQS 的作用。

    • AQS 抽象队列同步器 是一个用于构建锁和同步器的框架。维护了一个 FIFO 队列(CLH 队列),用于管理等待锁的线程。通过 state 变量 + CAS 机制 来管理锁状态。通过 CLH(双向链表)队列 让线程排队等待锁。

      1
      2
      3
      4
      // AQS的关键成员
      private volatile int state; // 0:未加锁,>0:加锁状态(重入次数)
      private Thread exclusiveOwnerThread; // 当前持有锁的线程
      private final transient Queue<Node> waitQueue; // CLH队列

      锁是面向使用者,其定义了使用者和锁交互的接口,隐藏了实现细节同步器则面向锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作

  2. 基础操作:

    • AQS 的状态:AQS 使用一个 volatileint 变量(state)来表示锁的状态,通过CAS操作来设置状态:
      • state = 0:锁未被占用。
      • state > 0:锁被占用,state 的值表示锁的重入次数。
      • compareAndSetState(int expect,int update)
    • AQS 的队列

      • AQS 维护了一个双向链表(CLH 队列),用于存储等待锁的线程。

      • 每个节点(Node)包含线程的引用和状态信息。

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        static final class Node {
        volatile int waitStatus; // -1 表示需要唤醒,0 表示正常
        volatile Node prev; // 前驱节点
        volatile Node next; // 后继节点
        volatile Thread thread; // 当前等待的线程
        }
        HEAD -> 线程 A -> 线程 B -> 线程 C -> ...
        private transient volatile Node head; // 队头线程将优先获得锁
        private transient volatile Node tail; // 抢锁失败的线程追加到队尾


      • 首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点。

      • 设置首节点是通过获取同步状态成功的线程来完成的,由于只有一个线程能够成功获取到同步状态,因此设置头节点的方法并不需要使用CAS来保证

      • 基于CAS的设置尾节点的方法compareAndSetTail(Node expect, Node update)

      • 在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行短暂自旋+阻塞(前驱节点非头节点);移出队列的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用 tryRelease(int arg) 方法释放同步状态,然后唤醒头节点的后继节点。

    • AQS方法:

      • acquire(int arg):线程获取独占锁,获取失败就进入 AQS 队列。

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        public final void acquire(int arg) {
        // 调用自定义同步器实现的 `tryAcquire(int arg)` 方法,该方法保证线程安全的获取同步状态
        if (!tryAcquire(arg) &&
        // 如果同步状态获取失败,则构造同步节点(独占式 `Node.EXCLUSIVE` ,同一时刻只能有一个线程成功获取同步状态)并通过 `addWaiter(Node node)` 方法将该节点加入到同步队列的尾部。
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        // 最后调用 `acquireQueued(Node node,int arg)` 方法,使得该节点以“死循环”的方式获取同步状态。如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。
        selfInterrupt();
        }

        final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;//标识是否失败
        try {
        boolean interrupted = false;
        for (;;) {//循环获取同步状态
        final Node p = node.predecessor();//获取前驱节点
        if (p == head && tryAcquire(arg)) {//当前驱节点为头节点时尝试获取同步状态
        //得到同步状态
        setHead(node);//设置此节点为头节点
        p.next = null; // help GC
        failed = false;//标识操作成功
        return interrupted;//未中断返回
        }
        // 🚀 前驱节点不是 head,进入阻塞状态 LockSupport.park(this);
        if (shouldParkAfterFailedAcquire(p, node) &&
        parkAndCheckInterrupt())
        interrupted = true;//中断
        }
        } finally {
        if (failed)
        cancelAcquire(node);
        }
        }
      • release(int arg):释放锁,并唤醒下一个等待线程。

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        public final boolean release(int arg) {
        if (tryRelease(arg)) {
        // 当前头结点线程
        Node h = head;
        if (h != null && h.waitStatus != 0)
        unparkSuccessor(h);//唤醒等待状态的头结点线程
        return true;
        }
        return false;
        }
      • tryAcquire(int arg):独占模式,尝试获取锁(子类实现)

      • tryRelease(int arg):独占模式,释放锁(子类实现)

      • acquireShared(int arg):共享模式,尝试获取共享锁

      • releaseShared(int arg):共享模式,释放共享锁

    锁的获取与释放

    • 获取锁
      • 线程通过 CAS 操作尝试将 state 从 0 改为 1。
      • 如果失败,则加入 AQS 队列并挂起。
    • 释放锁
      • 线程通过 CAS 操作将 state 减 1。
      • 如果 state 变为 0,则唤醒队首的线程。
    1
    2
    3
    4
    5
    6
    7
    lock() {
    if (CAS(state, 0, 1)) { // 🚀 CAS 尝试加锁
    setOwner(Thread.currentThread());
    return;
    }
    enterAQSQueue(); // 🚀 失败就进 AQS 队列
    }
    步骤 描述
    ① CAS 竞争锁 通过 compareAndSwap 直接尝试修改 state=1
    ② 竞争失败入队 线程进入 AQSCLH 队列
    ③ 线程阻塞 **当前线程进入 park()**(CPU 让出)
    ④ 线程被唤醒 排在队首的线程唤醒,再次尝试 CAS 获取锁
    ⑤ 获取成功 线程获得锁,执行代码
    1
    2
    3
    4
    5
    unlock() {
    if (decrementState() == 0) { // 🚀 递减 state
    wakeUpNext(); // 🚀 唤醒下一个排队的线程
    }
    }
    • state-- 直到 0,表示锁已释放。
    • 如果 AQS 队列中有线程,就唤醒队首线程,重新 CAS 竞争。
    • 锁的获取

      1. 尝试获取锁

        • 线程首先通过 CAS 操作尝试获取锁。
        • 如果锁的状态为 0(未被占用),则 CAS 成功,线程获取锁。
        • 如果锁的状态不为 0(已被占用),则 CAS 失败,进入 AQS 队列并挂起。
      2. 加入 AQS 队列

        • 如果 CAS 失败,线程会被封装为一个节点(Node)并加入 AQS 队列的队尾。
        • 线程在队列中等待,直到被唤醒。
      3. 唤醒并重新尝试获取锁

        • 当锁被释放时,AQS 会唤醒队首的线程。
        • 被唤醒的线程会再次尝试通过 CAS 获取锁。
    • 锁的释放

      1. 释放锁
        • 线程通过 CAS 操作将锁的状态从 1 改为 0。
        • 如果 AQS 队列中有等待的线程,则唤醒队首的线程。
  3. 公平锁与非公平锁ReentrantLock 支持 公平锁非公平锁 两种模式,默认是非公平锁。

    • 非公平锁

      • 特点
        • 新请求锁的线程会先尝试通过 CAS 获取锁,如果成功则直接抢占锁。
        • 即使 AQS 队列中有等待的线程,新线程也可能抢占锁。
      • 优点
        • 吞吐量高,减少线程切换的开销。
      • 缺点
        • 可能导致线程饥饿,某些线程长期等待。
      1
      2
      3
      4
      5
      6
      7
      ReentrantLock lock = new ReentrantLock(); // 默认是非公平锁
      lock.lock();
      try {
      // 同步代码块
      } finally {
      lock.unlock();
      }
    • 公平锁

      • 特点
        • 新请求锁的线程会直接进入 AQS 队列的队尾,按照 FIFO 的顺序获取锁。
        • 只有队首的线程可以尝试获取锁。
      • 优点
        • 公平性高,避免线程饥饿。适合高并发 + 低延迟的场景。
      • 缺点
        • 吞吐量较低,增加了线程切换的开销。
      1
      2
      3
      4
      5
      6
      7
      ReentrantLock lock = new ReentrantLock(true); // 公平锁
      lock.lock();
      try {
      // 同步代码块
      } finally {
      lock.unlock();
      }

5.1 优点

  • 可重入性:同一个线程可以多次获取同一把锁。
  • 灵活性:支持公平锁和非公平锁。
  • 可中断:支持 lockInterruptibly() 方法,允许线程在等待锁时响应中断。
  • 超时机制:支持 tryLock() 方法,允许线程在指定时间内尝试获取锁。

5.2 缺点

  • 复杂性:使用比 synchronized 复杂,需要手动管理锁的获取和释放。
  • 性能开销:在低竞争场景下,性能可能不如 synchronized
  1. 总结
特性 ReentrantLock synchronized
实现方式 基于 CAS 和 AQS 基于监视器锁(Monitor)
公平性 支持公平锁和非公平锁 非公平锁
可中断 支持 不支持
超时机制 支持 不支持
复杂性 较高 较低

ReentrantLock 提供了比 synchronized 更灵活的锁控制,适合需要高级功能的并发场景。如果只是简单的同步需求,synchronized 仍然是更好的选择。

ReentrantLockjava.util.concurrent.locks 包中的一个可重入互斥锁,实现了 Lock 接口,提供了显式锁管理和更多灵活的锁控制。

1. ReentrantLock 的核心特性

  1. **重入性 (Reentrancy)**:同一线程可多次获取锁,计数器记录锁的获取次数,释放时需多次解锁。

  2. 公平与非公平模式:支持公平和非公平两种模式。

    非公平锁(默认)

    • 优点:吞吐量更高,适合高并发环境。
    • 原理:不关心等待队列顺序,直接尝试 CAS 获取锁。

    公平锁(可选)

    • 优点:严格遵循 FIFO 顺序,避免线程饥饿。
    • 原理:检查同步队列中是否有等待线程,优先唤醒队首线程。
  3. 可中断锁获取:支持 lockInterruptibly() 方法,响应中断。

  4. 超时获取锁:支持 tryLock(time, unit),设置超时机制。

  5. **条件变量支持 (Condition)**:支持多条件等待/通知机制。

2. 内部实现核心组件

2.1 内部类 Sync

ReentrantLock 通过内部类 Sync 来实现锁逻辑,继承了 AbstractQueuedSynchronizer (AQS)

关键方法:

  • lock():获取锁,阻塞式。
  • tryAcquire():尝试获取锁,成功返回 true,否则 false
  • release():释放锁,减少计数器。
  • tryRelease():尝试释放锁,若计数器归零,表示锁已释放。

2.2 锁获取机制 (lock())

非公平锁流程 (默认模式)

  1. 尝试通过 compareAndSetState(0, 1) 获取锁。
  2. 若获取失败,则调用 acquire(1),进入 AQS 阻塞队列,等待锁。
  3. 获取成功时,将当前线程设置为持有锁的线程。

公平锁流程

  1. 调用

    1
    hasQueuedPredecessors()

    判断当前线程是否排在队首:

    • 若队列为空或当前线程为第一个节点,尝试获取锁。
    • 否则进入队列排队,等待锁释放。

2.3 锁释放机制 (unlock())

  1. 调用 release(1)

  2. 1
    tryRelease()

    中:

    • 锁计数器减 1。
    • 若计数器归零,表示锁已释放,将当前线程置为空。
    • 唤醒队列中的下一个等待线程。

3. ReentrantLock 内部机制详解

3.1 AQS 队列管理

AQS 使用 双向链表 维护等待线程队列:

  • 获取锁失败时,线程进入等待队列,节点类型为 Node.EXCLUSIVE
  • 锁释放时,队首节点被唤醒,尝试重新获取锁。

3.2 公平与非公平模式实现

  • 非公平模式: 忽略队列顺序,当前线程直接竞争锁。
  • 公平模式: 必须按照队列顺序,先到先得,避免“插队”。

3.3 重入锁支持

  • Sync 中的 state 记录重入次数。
  • 相同线程多次获取锁时,state 递增。
  • 每次释放锁时,state 递减,归零时完全释放锁。

4. 使用示例

基本使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
javaCopy codeimport java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();

public void criticalSection() {
lock.lock(); // 获取锁
try {
System.out.println(Thread.currentThread().getName() + " 正在执行...");
} finally {
lock.unlock(); // 释放锁
}
}

public static void main(String[] args) {
ReentrantLockExample example = new ReentrantLockExample();

Runnable task = example::criticalSection;

new Thread(task, "线程1").start();
new Thread(task, "线程2").start();
}
}

5. 使用场景

  1. 需要显式锁管理的场景:如超时锁获取和中断支持。
  2. 需要公平锁的场景:确保无线程插队。
  3. 需要多条件变量支持的场景:通过 Condition 支持多个等待队列。

6. 重要对比: synchronized vs ReentrantLock

特性 synchronized ReentrantLock
获取锁方式 隐式获取 (JVM 管理) 显式获取和释放
可中断 不支持 支持
超时获取锁 不支持 支持
条件变量支持 wait/notify 单一条件 Condition 支持
公平锁支持 不支持 (非公平) 支持公平/非公平
性能优化 JVM 内置优化 (锁升级等) 用户控制,灵活性高

优势:

  • 支持可重入
  • 支持中断与超时操作
  • 可选公平与非公平锁
  • 支持多个条件变量 (Condition)

不足:

  • 需要显式加锁和解锁,容易导致死锁
  • 相比 synchronized,编码复杂度更高

7. 结论

ReentrantLock 提供了强大的锁控制功能,如可中断锁、超时机制和多条件变量等。其内部基于 AQS 实现了高效的线程管理和锁竞争控制,适用于需要更高并发控制的场景。

问:ReentrantLock 是如何实现可重入性的 ?

内部自定义了同步器 Sync,加锁的时候通过CAS 算法 ,将线程对象放到一个双向链表 中,每次获取锁的时候 ,看下当前维 护的那个线程ID和当前请求的线程ID是否一样,一样就可重入了;

ReentrantLock 的可重入性实现原理:

1. 可重入性定义:
可重入性指的是同一线程在获取锁后,可以再次获取该锁而不会被阻塞,从而避免死锁。

2. 实现机制:
ReentrantLock 的可重入性是通过其内部的 AQS(AbstractQueuedSynchronizer) 来实现的。主要依赖于一个整数变量 state,以及当前持有锁的线程引用 exclusiveOwnerThread

3. 工作流程:

  • 初次获取锁:
    • 线程尝试获取锁时,如果 state 为 0,说明锁未被占用。成功获取锁后:
      • 将 state 设置为 1。
      • 将当前线程设置为 exclusiveOwnerThread。
  • 再次获取锁(重入):
    • 如果当前线程再次尝试获取锁:
      • 检查当前线程是否与 exclusiveOwnerThread 相同。
      • 如果相同,说明是同一线程重入。
      • 增加 state 的值,表示锁被多次重入。
  • 释放锁:
    • 每次 unlock() 调用,state 减 1。
    • 当 state 减到 0 时:
      • 表示锁完全释放。
      • 将 exclusiveOwnerThread 设置为 null。
      • 其他等待的线程可以竞争该锁。

4. 示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();

public void methodA() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " - Enter methodA");
methodB(); // 重入锁示例
} finally {
lock.unlock();
}
}

public void methodB() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " - Enter methodB");
} finally {
lock.unlock();
}
}

public static void main(String[] args) {
ReentrantLockExample example = new ReentrantLockExample();
new Thread(example::methodA, "Thread-1").start();
}
}

5. 关键点总结:

  • state 用于记录锁的重入次数。
  • exclusiveOwnerThread 记录持有锁的线程。
  • 只有持有锁的线程才能重入,避免了死锁和不必要的阻塞。

问:ReentrantLock如何避免死锁?

  • 响应中断lockInterruptibly()
  • 可轮询锁tryLock()
  • 定时锁tryLock(long time)

ReentrantLock避免死锁的方式:

  1. **尝试锁定 (tryLock)**:
    使用 tryLock() 可以尝试在指定时间内获取锁。如果超时未获得锁,线程可以放弃操作,避免长时间阻塞,从而减少死锁的发生。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    if (lock.tryLock(2, TimeUnit.SECONDS)) {
    try {
    // 执行业务逻辑
    } finally {
    lock.unlock();
    }
    } else {
    System.out.println("未能获取锁,避免死锁");
    }
  2. 锁的获取顺序
    在多线程环境中,确保多个线程获取多个锁时,采用相同的顺序来避免循环依赖。例如,按资源ID的顺序加锁,防止交叉持有锁。

  3. 使用 lockInterruptibly() 方法
    lockInterruptibly() 允许线程在尝试获取锁时可以响应中断,避免无限期等待锁,进而避免死锁。

    1
    2
    3
    4
    5
    6
    7
    8
    try {
    lock.lockInterruptibly();
    // 执行业务逻辑
    } catch (InterruptedException e) {
    System.out.println("线程被中断,避免死锁");
    } finally {
    lock.unlock();
    }
  4. 设置超时时间与恢复机制
    设计良好的恢复机制,如事务回滚、任务重试等,防止锁获取失败导致应用僵死。

  5. 减少锁的持有时间
    将锁的持有时间限制在最小的业务范围内,避免长期持有锁。

  6. 检测与诊断工具
    使用监控工具和死锁检测机制,在出现死锁时能及时发现和处理。

这些机制结合可以有效减少死锁发生,提升系统的稳定性和可靠性。

避免死锁的代码示例与说明

  1. 响应中断的锁 lockInterruptibly()
  • 说明: 使用 lockInterruptibly() 可在获取锁时响应中断,避免长时间等待。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.util.concurrent.locks.ReentrantLock;

public class InterruptibleLockDemo {
private static final ReentrantLock lock = new ReentrantLock();

public static void main(String[] args) {
Thread t1 = new Thread(() -> accessResource(), "Thread-1");
Thread t2 = new Thread(() -> accessResource(), "Thread-2");

t1.start();
t2.start();
t1.interrupt(); // 中断线程1
}

public static void accessResource() {
try {
lock.lockInterruptibly(); // 支持中断获取锁
System.out.println(Thread.currentThread().getName() + " acquired the lock.");
Thread.sleep(2000); // 模拟任务
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " was interrupted.");
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
  1. 可轮询锁 tryLock()
  • 说明: 尝试获取锁,获取成功返回 true,失败立即返回 false,避免死锁。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.util.concurrent.locks.ReentrantLock;

public class TryLockDemo {
private static final ReentrantLock lock = new ReentrantLock();

public static void main(String[] args) {
Runnable task = () -> {
if (lock.tryLock()) {
try {
System.out.println(Thread.currentThread().getName() + " acquired the lock.");
Thread.sleep(1000); // 模拟任务
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
} else {
System.out.println(Thread.currentThread().getName() + " failed to acquire the lock.");
}
};

new Thread(task, "Thread-1").start();
new Thread(task, "Thread-2").start();
}
}
  1. 定时锁 tryLock(long time, TimeUnit unit)
  • 说明: 在指定时间内尝试获取锁,获取成功返回 true,超时返回 false

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class TimedLockDemo {
private static final ReentrantLock lock = new ReentrantLock();

public static void main(String[] args) {
Runnable task = () -> {
try {
if (lock.tryLock(2, TimeUnit.SECONDS)) {
try {
System.out.println(Thread.currentThread().getName() + " acquired the lock.");
Thread.sleep(3000); // 模拟任务
} finally {
lock.unlock();
}
} else {
System.out.println(Thread.currentThread().getName() + " timed out while waiting for the lock.");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
};

new Thread(task, "Thread-1").start();
new Thread(task, "Thread-2").start();
}
}

总结

  • 响应中断 lockInterruptibly(): 在等待锁期间支持线程中断。
  • 可轮询锁 tryLock(): 尝试获取锁,立即返回,避免长时间阻塞。
  • 定时锁 tryLock(long, TimeUnit): 在特定时间内尝试获取锁,避免无限等待。

问:tryLock 和 lock 和 lockInterruptibly 的区别?⭐⭐

  1. tryLock 能获得锁就返回 true,不能就立即返回 false
  2. tryLock(long timeout,TimeUnit unit),可以增加时间限制,如果超过该时间段还没获得锁,返回 false
  3. lock 能获得锁就返回 true,不能的话一直等待获得锁
  4. lock 和 lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程, lock 不会抛出异常,而 lockInterruptibly 会抛出异常。
  1. lock()
  • 作用: 获取锁,如果锁被占用,当前线程将进入等待状态,直到获得锁。
  • 特点: 不可中断,必须等待锁释放。
  • 适用场景: 需要确保获取锁,无论等待时间多长。

示例代码:

1
2
3
4
5
6
7
Lock lock = new ReentrantLock();
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock();
}
  1. tryLock()
  • 作用: 尝试获取锁,如果锁可用,则立即获取;否则,返回 false
  • 特点: 非阻塞,避免线程长时间等待锁。
  • 适用场景: 非阻塞操作,适合尝试性加锁。

示例代码:

1
2
3
4
5
6
7
8
9
if (lock.tryLock()) {
try {
// 临界区代码
} finally {
lock.unlock();
}
} else {
System.out.println("未能获取锁");
}
  1. tryLock(long time, TimeUnit unit)
  • 作用: 在指定的时间内尝试获取锁,如果成功返回 true,否则返回 false
  • 特点: 支持超时机制,适合限时操作。
  • 适用场景: 限时等待,避免无限期阻塞。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
try {
if (lock.tryLock(5, TimeUnit.SECONDS)) {
try {
// 临界区代码
} finally {
lock.unlock();
}
} else {
System.out.println("获取锁超时");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
  1. lockInterruptibly()
  • 作用: 支持中断的加锁方法。如果锁被占用,当前线程进入等待状态,但可以被其他线程中断。
  • 特点: 支持中断,避免死锁风险。
  • 适用场景: 需要对中断信号做出响应,适用于线程池管理等场景。

示例代码:

1
2
3
4
5
6
7
8
9
10
try {
lock.lockInterruptibly();
try {
// 临界区代码
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
System.out.println("线程被中断");
}

区别总结:

方法名 是否阻塞 支持中断 超时机制 返回值
lock() 阻塞
tryLock() 非阻塞 true/false
tryLock(time,unit) 阻塞 true/false
lockInterruptibly() 阻塞

选择时需根据具体需求权衡阻塞性、中断支持和超时管理等功能。


2.5 Condition

问:讲一下Condition?

Condition接口主要是和Lock配合实现等待通知模式,类似于对象监视器方法和synchronized的组合。其定义了一系列等待和唤醒方法,Condition对象依赖于Lock对象,一个Condition包含一个等待队列,Condition拥有首节点(firstWaiter)和尾节点(lastWaiter)。等待时将节点从同步队列移动到等待队列,需要把当前线程构建成一个新的等待结点。通知时则不需要新建节点,可以看作直接把等待节点移动到同步队列。

Condition 在 Java 中的作用

Condition 是 Java 并发包 (java.util.concurrent.locks) 提供的一个接口,用于支持线程之间的协调和通信,类似于 Object 类中的 wait()notify()notifyAll(),但功能更强大。Condition 是与 ReentrantLock 一起使用的,提供了线程等待和通知的灵活机制。

主要方法:

  1. await()
    • 当前线程进入等待状态,释放锁,直到被通知或被中断。
  2. signal()
    • 唤醒一个等待在此 Condition 上的线程。
  3. signalAll()
    • 唤醒所有等待在此 Condition 上的线程。
  4. awaitUninterruptibly()
    • await() 类似,但不会响应中断。
  5. await(long time, TimeUnit unit)
    • 超时等待,时间到后自动唤醒。

使用示例:生产者-消费者模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class Buffer {
private int data;
private boolean isEmpty = true;

private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();

public void produce(int value) throws InterruptedException {
lock.lock();
try {
while (!isEmpty) {
notFull.await(); // 等待缓冲区非满
}
data = value;
isEmpty = false;
System.out.println("Produced: " + data);
notEmpty.signal(); // 唤醒消费者
} finally {
lock.unlock();
}
}

public int consume() throws InterruptedException {
lock.lock();
try {
while (isEmpty) {
notEmpty.await(); // 等待缓冲区非空
}
isEmpty = true;
System.out.println("Consumed: " + data);
notFull.signal(); // 唤醒生产者
return data;
} finally {
lock.unlock();
}
}
}

public class ConditionExample {
public static void main(String[] args) {
Buffer buffer = new Buffer();

Thread producer = new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
buffer.produce(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});

Thread consumer = new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
buffer.consume();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});

producer.start();
consumer.start();
}
}

Objectwait()/notify() 对比:

Feature Condition Object wait()/notify()
锁机制 必须与 ReentrantLock 一起使用 必须与 synchronized 一起使用
多个等待队列 支持多个 Condition 实例 只有一个隐式等待队列
响应中断与超时支持 支持中断与超时 支持中断,但不支持超时
灵活性和控制力 较低

总结:

Condition 提供了灵活的线程间通信机制,可以替代 synchronizedwait()/notify(),在复杂并发场景中表现更好。其多等待队列和精准通知的特性非常适合生产者-消费者等模式。

问:对象监视器方法和Condition的异同 ?

任意一个Java对象,都拥有一组监视器方法(定义在 java.lang.Object 上),主要包括 wait()wait(long timeout)notify() 以及 notifyAll() 方法,这些方法与 synchronized 同步关键字配合,可以实现等待/通知模式

Condition 接口也提供了类似 Object 的监视器方法,与Lock配合可以实现等待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的。

Object的监视器方法与Condition接口的对比

对象监视器方法(wait(), notify(), notifyAll())和 Condition 在 Java 并发编程中都用于线程间通信,但它们的用法和实现机制存在显著差异:

相同点:

  1. 线程间通信: 两者都用于在多个线程之间共享信息,协调运行顺序。
  2. 等待与通知机制: 都提供等待和唤醒机制,以实现线程间的同步。

不同点:

特性 对象监视器方法(Object) Condition
所在类 Object 类的固有方法 java.util.concurrent.locks.Condition
使用范围 必须配合 synchronized 块或方法 必须配合 Lock 实现
多个等待队列支持 不支持(只能有一个等待队列) 支持多个 Condition 实例
唤醒机制 notify() 唤醒一个,notifyAll() 唤醒全部 signal() 唤醒一个,signalAll() 唤醒全部
响应中断支持 支持,wait() 可响应中断 支持,await() 可响应中断
等待超时支持 支持 wait(long timeout) 支持 await(long time, TimeUnit)
实现复杂度 相对简单,适用于基础同步场景 更灵活,适用于复杂同步需求

选择建议:*

  • 对象监视器方法: 适用于简单的线程同步和少量共享资源。
  • Condition: 适用于复杂的线程间协调,需要多个等待条件时尤其有用,如生产者-消费者模型中的多个条件队列。

2.6 AQS

问:讲讲AQS?实现原理?讲讲AQS怎么实现的Fair和NoFair?

AQS即队列同步器(AbstractQueuedSynchronizer),是构建锁和其他同步组件的基础框架,锁是面向使用者的,同步器则是面向锁的实现者。

实现原理:AQS底层是一个双向链表实现的FIFO同步队列,通过整型成员变量state来判断锁的同步状态,基于模板方法模式来设计,包含独占式和共享式的获取同步状态,各种锁自定义实现AQS。

对于非可重入锁状态不是0则去阻塞;对于可重入锁如果是0则执行,非0则判断当前线程是否是获取到这个锁的线程,是的话把state状态+1,比如重入5次,那么state=5。 而在释放锁的时候,同样需要释放5次直到state=0其他线程才有资格获得锁。

AbstractQueuedSynchronizer (AQS) 概述

AQS(AbstractQueuedSynchronizer)是Java并发包java.util.concurrent中的一个核心框架,用于实现同步工具如ReentrantLockSemaphoreCountDownLatch等。AQS提供了一个FIFO等待队列和状态管理机制,简化了线程同步逻辑的实现。

AQS 实现原理

  1. 同步状态管理:
    • AQS维护一个state字段(volatile int state),表示共享资源的状态。
    • 通过getState()setState()compareAndSetState()操作该状态,保证线程安全。
  2. 等待队列:
    • AQS维护一个双向链表形式的FIFO等待队列,节点类型为Node
    • 队列中的节点存储着等待线程及其相关状态信息。
  3. 模板方法模式:
    • 子类通过实现tryAcquiretryReleasetryAcquireSharedtryReleaseShared等方法,自定义同步逻辑。
  4. 独占模式(Exclusive):
    • 线程独占资源,适用于ReentrantLock
    • 获取锁:acquire(int arg),释放锁:release(int arg)
  5. 共享模式(Shared):
    • 多个线程共享资源,适用于SemaphoreCountDownLatch
    • 获取资源:acquireShared(int arg),释放资源:releaseShared(int arg)

公平锁与非公平锁的实现(Fair & NoFair)

  1. 公平锁(Fair)
  • 公平锁遵循FIFO顺序,先来先得。
  • 实现方式:在获取锁时,检查等待队列中是否有其他线程。若有,排队等候;若无,尝试获取锁。

示例:ReentrantLock 的公平锁实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
setState(c + acquires);
return true;
}
return false;
}
  1. 非公平锁(NoFair)
  • 非公平锁不保证线程的获取顺序,允许线程“插队”。
  • 实现方式:尝试直接获取锁,而不检查队列状态。

示例:ReentrantLock 的非公平锁实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
setState(c + acquires);
return true;
}
return false;
}

小结:AQS 的核心要点

  • 状态管理:通过state字段和CAS操作管理资源。
  • 等待队列:维护一个FIFO队列存储等待线程。
  • 可扩展性:通过模板方法模式,支持多种同步器的实现。
  • 公平性实现:通过队列和检查机制支持公平与非公平锁策略。

问:AQS两种资源共享方式?

  • Exclusive:独占,只有一个线程能执行,如ReentrantLock。
  • Share:共享,多个线程可以同时执行,如Semaphore、CountDownLatch、ReadWriteLock,CyclicBarrier。

在Java并发包中,AQS(AbstractQueuedSynchronizer) 提供了两种资源共享方式:

  1. 独占(Exclusive)模式
  • 定义: 只有一个线程能获取资源,其它线程必须等待。
  • 实现:
    • 当一个线程成功获取资源后,其他线程将被阻塞,进入等待队列。
    • 使用场景:ReentrantLock(独占锁)。
  • 主要方法:
    • tryAcquire(int arg):尝试获取资源,返回true表示成功。
    • tryRelease(int arg):释放资源。
  1. 共享(Shared)模式
  • 定义: 多个线程可以同时获取资源,直到资源耗尽。
  • 实现:
    • 多个线程同时获取资源,资源分配完后,剩余线程进入等待队列。
    • 使用场景:CountDownLatchSemaphoreReadWriteLock(读锁)。
  • 主要方法:
    • tryAcquireShared(int arg):尝试共享式获取资源,返回值 >= 0 表示成功。
    • tryReleaseShared(int arg):释放共享资源。

区别总结

特性 独占模式 共享模式
资源占用 单线程占用 多线程共享
实现方法 tryAcquire tryAcquireShared
使用场景 ReentrantLock CountDownLatchSemaphore

这两种模式通过自定义AQS的tryAcquiretryAcquireShared方法,灵活实现各种同步器。

问:CAS了解么? CAS 有什么缺陷,如何解决?还了解其他同步机制么?

CAS,Compare and Set,比较并交换,常用来实现乐观锁。

实现原理:内存值,预期值,修改的新值,当内存值等于预期值时,将内存值修改为新值,否则什么都不做。

CAS的缺陷有三个:

  1. ABA问题,即A变为B再变为A也能满足判断条件。
  2. 如果CAS失败,自旋会给CPU带来压力,死循环。
  3. 只能保证对一个变量的原子性操作。

解决方案分别是:

  1. 引入版本号或修改次数,1A->2B->3A。
  2. 使用JVM提供的pause指令。
  3. 用锁将多个共享变量合并为一个来操作,使用 AtomicReference 类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。

CAS在java中的应用:
(1):Atomic系列

还有信号量机制,自旋锁等。

CAS (Compare and Swap) 概念

CAS (Compare and Swap/Set) 是一种无锁的并发编程技术,常用于实现高效的原子操作。它通过硬件支持的指令(如x86的cmpxchg)实现。

工作原理:

  • 比较内存中的值(expectedValue)是否等于期望值。
  • 如果相等,将内存值更新为新值(newValue)。
  • 如果不相等,说明其他线程已修改该值,操作失败,需重试。

CAS 的缺陷与解决方案

  1. ABA问题:
    • 描述: 内存中的值从A变为B,然后又变回A。CAS仅检查值是否等于预期值,因此无法察觉中间变化。
    • 解决:
      • 引入版本号,如AtomicStampedReference,通过版本号检查变化。
  2. 自旋等待(Spin-waiting)问题:
    • 描述: 若线程长时间CAS失败,将不断重试,浪费CPU资源。
    • 解决:
      • 增加重试次数上限,必要时让线程休眠。
      • 使用更高级别的锁机制,如ReentrantLock
  3. 只能保证一个变量的原子性:
    • 描述: CAS一次只能操作一个变量。
    • 解决:
      • 使用AtomicReference封装对象。
      • 对多个变量,使用AtomicReferenceFieldUpdater或同步机制。
  4. 性能问题:
    • 描述: 在高并发场景中,若竞争激烈,CAS失败率高,重试频繁,性能下降。
    • 解决:
      • 使用高效的并发容器,如ConcurrentHashMapConcurrentLinkedQueue,它们在内部优化了CAS重试机制。

其他同步机制

  1. 同步锁: synchronizedReentrantLock(独占锁)。
  2. 读写锁: ReentrantReadWriteLock
  3. 倒计时器: CountDownLatch
  4. 信号量: Semaphore(控制并发线程数)。
  5. 栅栏: CyclicBarrier(线程集合点)。
  6. 原子变量类: AtomicIntegerAtomicReference
  7. 锁支持工具类: LockSupport

这些机制在不同场景下实现了线程间的同步与并发控制。需要根据实际情况选择合适的同步工具。

问:CAS和synchronized有什么区别?都用synchronized不行么?

synchronized是获取对象锁,属于重量级锁,虽然有进行优化,但还是有一定的性能开销。CAS则并未利用锁,其机制比较适合读多写少的场景。

如果都采用synchronized,在并发编程时不能获得令人满意的执行效率。

CAS 与 synchronized 的区别

特性 CAS(Compare and Swap) synchronized
类型 无锁机制(乐观锁) 阻塞锁(悲观锁)
实现方式 基于硬件指令,使用自旋重试 JVM 内置锁,使用锁机制
线程阻塞情况 不会阻塞,失败时自旋重试 失败时阻塞,等待锁释放
性能 在低竞争环境中性能优越 在高竞争环境中可能更稳定
适用场景 轻量级并发操作,如计数器、累加器 需要强同步的代码块或方法
开销 CPU开销高(失败时自旋重试) 线程切换和上下文切换开销
粒度控制 只能控制变量或内存区域 控制代码块、方法,粒度灵活
原子性保障 仅能保证单个变量的原子性 保证整个代码块的原子性

为什么不用 synchronized 替代 CAS?

  1. 性能差异:
    • synchronized在JDK早期版本是重量级锁,性能较差。虽然在JDK 1.6后进行了优化(如偏向锁、轻量级锁、锁消除等),但在高并发场景下,仍有线程阻塞和上下文切换成本。
  2. 非阻塞性:
    • CAS是非阻塞算法,操作失败时会自旋重试,适用于无锁结构,避免了线程阻塞和上下文切换。
  3. 扩展性:
    • 在简单的计数、累加等场景,CAS更高效,常用于 AtomicIntegerConcurrentHashMap 等类中。
  4. 乐观锁模型:
    • CAS采用乐观锁模型,假设大多数操作不会冲突,适用于读多写少的环境。

何时选择 CAS 与 synchronized?

  • 用 CAS:
    • 轻量级操作,如计数器、标志位更新。
    • 高并发环境,避免锁竞争。
  • 用 synchronized:
    • 需要控制多个变量的一致性。
    • 操作复杂,业务逻辑多,需要代码块级别的同步控制。

两者适用场景不同,不能简单互换。CAS适合轻量级、无锁的高性能操作,而synchronized适合更广泛、严格的同步需求。


2.7 CountDownLatch

问:用过CountDownLatch么?什么场景下用的?

CountDownLatch 概念

CountDownLatch 是 Java 并发包中的同步工具类,用于协调多个线程之间的执行。它允许一个或多个线程等待其他线程完成某些操作后再继续执行。

CountDownLatch 的工作原理

  • 它包含一个计数器,初始值由构造函数设定,如 CountDownLatch(int count)
  • 调用 countDown() 方法时,计数器递减。
  • 调用 await() 方法的线程会阻塞,直到计数器变为零。

使用场景

1. 等待多个任务完成后执行操作

  • 示例: 主线程等待多个工作线程完成任务,然后合并结果。
  • 场景: 并行计算任务、批处理任务等。

2. 模拟并发测试

  • 示例: 模拟高并发环境,在所有线程准备好后同时执行操作。
  • 场景: 压力测试、负载测试。

3. 系统服务启动检查

  • 示例: 在启动主应用程序前,确保数据库、缓存服务等组件都已启动。
  • 场景: 系统初始化、依赖服务检查。

4. 任务分解与结果汇总

  • 示例: 将一个大任务分解为多个小任务,等所有小任务完成后汇总结果。
  • 场景: 文件处理、数据分片处理。

示例代码: 等待任务完成后执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
int taskCount = 3;
CountDownLatch latch = new CountDownLatch(taskCount);

for (int i = 1; i <= taskCount; i++) {
final int taskId = i;
new Thread(() -> {
try {
System.out.println("任务 " + taskId + " 正在执行...");
Thread.sleep((long)(Math.random() * 1000));
System.out.println("任务 " + taskId + " 完成!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
latch.countDown(); // 计数器递减
}
}).start();
}

latch.await(); // 等待所有任务完成
System.out.println("所有任务已完成,继续主线程执行...");
}
}

CountDownLatch 的注意事项

  1. 不可重用: CountDownLatch 一旦计数器变为零,就不能重置。如果需要重用,考虑使用 CyclicBarrier
  2. 线程安全: 内部使用 AQS 保证线程安全。
  3. 适用单向同步: 如果需要线程之间多次同步,考虑使用 CyclicBarrierPhaser

总结:

CountDownLatch 适用于任务等待、启动检查和并发测试等场景,是实现线程同步的重要工具。

问:什么是信号量Semaphore?应用场景?

信号量是一种固定资源的限制的一种并发工具包,基于AQS实现的,在构造的时候会设置一个值,代表着资源数量。信号量主要是应用于是用于多个共享资源的互斥使用,和用于并发线程数的控制(druid的数据库连接数,就是用这个实现的),信号量也分公平和非公平的情况,基本方式和reentrantLock差不多,在请求资源调用task时,会用自旋的方式减1,如果成功,则获取成功了,如果失败,导致资源数变为了0,就会加入队列里面去等待。调用release的时候会加一,补充资源,并唤醒等待队列。

Semaphore 应用

  • acquire() release() 可用于对象池,资源池的构建,比如静态全局对象池,数据库连接池;
  • 可创建计数为1的S,作为互斥锁(二元信号量)

信号量(Semaphore)概念

Semaphore 是 Java 并发包中的同步工具类,用于控制对共享资源的访问数量,维护一个许可计数。线程获取许可时计数减一,释放许可时计数加一。当计数为零时,获取许可的线程将被阻塞。

Semaphore 的工作原理

  • 构造方法:
    • Semaphore(int permits):指定许可数量。
    • Semaphore(int permits, boolean fair):指定许可数量和公平性策略。
  • 主要方法:
    • acquire():尝试获取一个许可,若没有可用许可,阻塞等待。
    • release():释放一个许可,增加计数。
    • tryAcquire():尝试立即获取一个许可,成功返回true,否则返回false

应用场景

1. 限制访问资源的线程数

  • 示例: 限制数据库连接池的最大连接数,避免资源过载。
  • 场景: 服务器连接池、线程池控制。

2. 流量控制与限流

  • 示例: 限制对API的请求数,控制并发请求。
  • 场景: 限流保护、高并发接口。

3. 多资源共享的访问控制

  • 示例: 控制停车场的车位数量,每次允许多个车辆进入停车场。
  • 场景: 资源池管理、存储容量管理。

4. 线程间同步

  • 示例: 线程之间按顺序执行,或在任务完成后释放许可。
  • 场景: 生产者-消费者模型中的同步控制。

示例代码: 停车场控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.util.concurrent.Semaphore;

public class ParkingLot {
private static final int TOTAL_SPOTS = 3;
private static final Semaphore parkingSpots = new Semaphore(TOTAL_SPOTS);

public static void main(String[] args) {
for (int i = 1; i <= 6; i++) {
final int carId = i;
new Thread(() -> {
try {
System.out.println("车辆 " + carId + " 等待进入停车场...");
parkingSpots.acquire(); // 获取许可
System.out.println("车辆 " + carId + " 停入停车场...");
Thread.sleep((long)(Math.random() * 5000)); // 模拟停车时间
System.out.println("车辆 " + carId + " 离开停车场...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
parkingSpots.release(); // 释放许可
}
}).start();
}
}
}

Semaphore 的注意事项

  1. 公平性: 非公平模式性能更高,适用于无严格先后要求的场景。
  2. 许可证可重入: 信号量许可不是线程绑定的,任何线程都可以释放许可。
  3. 使用场景: 避免资源过载,控制最大并发量,解决生产者-消费者模型中的资源分配问题。

总结:

Semaphore 在并发编程中是一个强大的工具,适用于资源控制、并发限流等场景,提供了灵活的许可管理机制,确保共享资源的高效利用与安全访问。

问:用过CyclicBarrier吗? 和 countdownlatch 的区别?

个人理解 赛马和点火箭

  • con用于主线程等待其他子线程任务都执行完毕后再执行,cyc用于一组线程相互等待大家都达到某个状态后,再同时执行;
  • CountDownLatch是不可重用的,CyclicBarrier可重用

CountDownLatch是等待其他线程执行到某一个点的时候,在继续执行逻辑(子线程不会被阻塞,会继续执行),只能被使用一次。最常见的就是join形式,主线程等待子线程执行完任务,在用主线程去获取结果的方式(当然不一定),内部是用计数器相减实现的(没错,又特么是AQS),AQS的state承担了计数器的作用,初始化的时候,使用CAS赋值,主线程调用await()则被加入共享线程等待队列里面,子线程调用countDown的时候,使用自旋的方式,减1,知道为0,就触发唤醒。

CyclicBarrier回环屏障,主要是等待一组线程到底同一个状态的时候,放闸。CyclicBarrier还可以传递一个Runnable对象,可以到放闸的时候,执行这个任务。CyclicBarrier是可循环的,当调用await的时候如果count变成0了则会重置状态,如何重置呢,CyclicBarrier新增了一个字段parties,用来保存初始值,当count变为0的时候,就重新赋值。还有一个不同点,CyclicBarrier不是基于AQS的,而是基于RentrantLock实现的。存放的等待队列是用了条件变量的方式。

Java 并发编程 中,CountDownLatchSemaphoreCyclicBarrier 都是 java.util.concurrent 包提供的 线程同步工具,但它们的用途和实现机制有所不同。

🔥 1. 核心区别对比

特性 CountDownLatch Semaphore CyclicBarrier
主要用途 等待多个线程完成任务,然后继续执行 限制并发访问的线程数 让一组线程互相等待,直到所有线程都到达屏障
计数器变化 只能减小,不能重置 支持增加和减少 达到阈值后可重置(可复用)
是否可重用 ❌ 不可重用 ✅ 可重用 ✅ 可重用
线程等待机制 await() 等待计数器归零 acquire() / release() 控制线程数 所有线程 await(),全部到达后一起执行
典型应用场景 主线程等待多个子任务完成(如 join() 替代) 限流、控制并发量(如数据库连接池) 多线程任务同步,如多线程计算最终合并结果

✅ 2. CountDownLatch(倒计时器)

🔹 作用

  • 用于多个线程执行任务后,通知主线程继续执行
  • 计数器只能递减,不能重置,用于 一次性事件

🔹 使用场景

  • 主线程等待多个子线程完成(类似 join())。
  • 并行任务,主线程等待所有任务完成后继续(如批处理任务)。

🔹 使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
private static final int THREAD_COUNT = 3;
private static final CountDownLatch latch = new CountDownLatch(THREAD_COUNT);

public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 执行任务");
latch.countDown(); // 计数器减1
}).start();
}

latch.await(); // 等待所有任务完成
System.out.println("所有任务完成,主线程继续");
}
}

🔹 结果

1
2
3
4
Thread-0 执行任务
Thread-1 执行任务
Thread-2 执行任务
所有任务完成,主线程继续

🔹 关键方法

方法 作用
countDown() 计数器 -1,当 count == 0 时,await() 解除阻塞
await() 阻塞当前线程,直到计数器变为 0

🔹 适用场景

  • 主线程等待多个任务完成(如批量任务)。
  • 多个线程同步启动(倒计时开始)。

✅ 3. Semaphore(信号量)

🔹 作用

  • 用于控制并发线程数,防止资源被过多线程占用(如数据库连接池)。
  • 线程 acquire() 获取许可证,用完后 release() 归还许可证

🔹 使用场景

  • 限流(最多 N 个线程访问某资源)。
  • 数据库连接池(控制最大连接数)。
  • 停车场(控制可停车辆数)。

🔹 使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.concurrent.Semaphore;

public class SemaphoreExample {
private static final Semaphore semaphore = new Semaphore(2); // 允许 2 个线程同时执行

public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
semaphore.acquire(); // 获取许可证
System.out.println(Thread.currentThread().getName() + " 获取资源");
Thread.sleep(2000);
} catch (InterruptedException ignored) {
} finally {
System.out.println(Thread.currentThread().getName() + " 释放资源");
semaphore.release(); // 释放许可证
}
}).start();
}
}
}

🔹 结果

1
2
3
4
5
6
7
Thread-0 获取资源
Thread-1 获取资源
Thread-0 释放资源
Thread-2 获取资源
Thread-1 释放资源
Thread-3 获取资源
...

🔹 关键方法

方法 作用
acquire() 获取许可证(阻塞等待)
tryAcquire() 尝试获取许可证(不阻塞)
release() 释放许可证

🔹 适用场景

  • 限流、并发控制(数据库连接池、限流算法)。
  • 多个线程访问共享资源(如 I/O)

✅ 4. CyclicBarrier(循环栅栏)

🔹 作用

  • 让一组线程相互等待,直到所有线程都到达屏障(Barrier)后再继续执行
  • 可以重复使用,不像 CountDownLatch 只能用一次。

🔹 使用场景

  • 多线程计算,等待所有线程完成某个阶段后一起执行下一步
  • 模拟多人游戏加载,所有玩家加载完毕后开始游戏

🔹 使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierExample {
private static final int THREAD_COUNT = 3;
private static final CyclicBarrier barrier = new CyclicBarrier(THREAD_COUNT, () -> {
System.out.println("所有线程已到达,开始执行下一步");
});

public static void main(String[] args) {
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 准备就绪");
try {
barrier.await(); // 等待所有线程到达
} catch (InterruptedException | BrokenBarrierException ignored) {
}
System.out.println(Thread.currentThread().getName() + " 继续执行");
}).start();
}
}
}

🔹 结果

1
2
3
4
5
6
7
Thread-0 准备就绪
Thread-1 准备就绪
Thread-2 准备就绪
所有线程已到达,开始执行下一步
Thread-0 继续执行
Thread-1 继续执行
Thread-2 继续执行

🔹 关键方法

方法 作用
await() 等待所有线程到达屏障
reset() 重置屏障,允许再次使用

🔹 适用场景

  • 多线程计算任务同步(如并行 MapReduce)。
  • 游戏多人匹配,等所有人准备好后开始

✅ 5. 总结

特性 CountDownLatch Semaphore CyclicBarrier
作用 等待多个线程完成任务 控制并发线程数 让一组线程互相等待
计数器变化 只能递减,不能重置 可增减 可重置
是否可重用 ❌ 否 ✅ 是 ✅ 是
线程等待 await() 阻塞 acquire() 获取,release() 释放 await() 等待所有线程到达
适用场景 主线程等待子任务完成 限流、并发控制 多线程同步等待

🎯 结论

  • CountDownLatch主线程等待多个任务完成(不可复用)。
  • Semaphore限流,控制并发线程数(类似令牌桶)。
  • CyclicBarrier线程相互等待,所有人准备好一起执行(可复用)。

如果你在 面试 / 设计系统时 需要选择合适的并发工具,建议结合具体业务场景!🚀

CyclicBarrier 概念

CyclicBarrier 是 Java 并发包中的同步工具类,用于让一组线程互相等待,直到所有线程到达屏障点(Barrier),然后继续执行。

  • “Cyclic” 表示它可以重复使用,适合需要多次同步的场景。
  • 构造方法:
    • CyclicBarrier(int parties):指定需要等待的线程数。
    • CyclicBarrier(int parties, Runnable barrierAction):在所有线程到达屏障点后执行指定任务。

主要方法

  • await()
    每个线程调用此方法,表示到达屏障点。线程会阻塞,直到所有线程都调用了 await(),然后继续执行。

CyclicBarrier 和 CountDownLatch 的区别

特性 CyclicBarrier CountDownLatch
作用 等待所有线程到达屏障点后再继续执行 一个或多个线程等待其他线程完成任务
可重用性 可重复使用 不可重复使用
触发操作 可指定一个屏障操作(Runnable),在屏障点执行 无触发操作
线程数量控制 必须指定固定数量的线程,所有线程都要到达屏障点 允许动态调整计数值
适用场景 多线程分阶段任务,需要同步点 等待任务完成后继续,或协调线程启动顺序

应用场景

1. 多线程分阶段计算

  • 各线程完成各自的子任务后等待,所有线程都完成后再进入下一阶段。
  • 示例: 数据分片处理、分步任务调度。

2. 模拟多线程并发

  • 所有线程同步启动,保证同时开始执行关键操作。
  • 示例: 性能测试工具中模拟并发场景。

3. 循环执行的任务协调

  • 每个阶段结束后,重新开始下一阶段,直到任务完成。
  • 示例: 多回合比赛或分步算法。

示例代码: 数据分片处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierExample {
public static void main(String[] args) {
int numThreads = 3;
CyclicBarrier barrier = new CyclicBarrier(numThreads, () -> {
System.out.println("所有线程已完成本阶段任务,继续下一阶段...");
});

for (int i = 0; i < numThreads; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " 正在执行任务...");
Thread.sleep((long) (Math.random() * 1000)); // 模拟任务执行
System.out.println(Thread.currentThread().getName() + " 到达屏障点...");
barrier.await(); // 等待其他线程
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}

选择 CyclicBarrier 或 CountDownLatch

  • 选择 CyclicBarrier:
    • 任务分阶段完成,线程需要重复同步。
    • 需要在屏障点执行额外操作。
  • 选择 CountDownLatch:
    • 一次性任务,如主线程等待所有子线程完成后继续执行。
    • 不需要重复使用同步机制。

总结:

  • CyclicBarrier 适用于循环同步或阶段性任务,线程必须同时到达屏障点才能继续执行。
  • CountDownLatch 更适合一次性任务的线程协调,特别是主线程等待子线程完成时使用。
    根据具体需求选择工具,确保线程同步的效率和正确性。

CyclicBarrier 概念

CyclicBarrier 是 Java 并发包中的同步工具,用于让一组线程在执行到某个共同点时等待彼此,直到所有线程都到达该点后再继续执行。Cyclic 表示它可以被重用,线程到达屏障后,计数器会被重置。

CyclicBarrier 的主要方法

  • 构造函数:
    • CyclicBarrier(int parties):指定等待的线程数量。
    • CyclicBarrier(int parties, Runnable barrierAction):在线程达到屏障时执行一个动作。
  • 主要方法:
    • await():每个线程调用此方法,表示到达屏障。线程会阻塞,直到所有线程都到达。

CyclicBarrier 示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierExample {
public static void main(String[] args) {
int threadCount = 3;
CyclicBarrier barrier = new CyclicBarrier(threadCount, () ->
System.out.println("所有线程到达屏障点,开始执行下一阶段任务...")
);

for (int i = 1; i <= threadCount; i++) {
final int threadId = i;
new Thread(() -> {
try {
System.out.println("线程 " + threadId + " 正在执行任务...");
Thread.sleep((long)(Math.random() * 2000));
System.out.println("线程 " + threadId + " 到达屏障点...");
barrier.await(); // 等待其他线程
System.out.println("线程 " + threadId + " 继续执行...");
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}

CountDownLatch 与 CyclicBarrier 的区别

特性 CountDownLatch CyclicBarrier
主要功能 等待其他线程完成任务后继续执行 等待线程之间互相到达屏障点
线程数量 指定计数,不能重用 可重用,自动重置
计数递减方式 调用 countDown() 递减计数 每个线程调用 await()
阻塞机制 主线程等待其他线程完成 所有线程相互等待
执行动作 无附加操作 到达屏障时可执行动作
适用场景 任务完成等待、主线程汇总结果 多线程阶段性任务协调

选择使用场景

使用 CountDownLatch:

  • 等待一组线程完成,主线程继续执行。
  • 一次性使用,如任务汇总、服务启动检查等。

使用 CyclicBarrier:

  • 多线程阶段性任务协调,线程之间需要互相等待。
  • 多次使用的场景,如并行计算、循环任务调度等。

总结:

  • CountDownLatch 适合 等待其他线程完成任务
  • CyclicBarrier 适合 线程之间的协作与同步,并且可以重用。选择时应根据应用程序的同步要求和执行模式进行选择。

2.8 锁的应用

问:ConcurrentHashMap的get需要加锁么,为什么?

ConcurrentHashMap 的 get() 是否需要加锁?

结论:
ConcurrentHashMapget() 方法 不需要加锁,因为它是 线程安全的,在大多数实现中是 无锁的读操作

为什么 get() 不需要加锁?

1. 基于分段锁的实现 (JDK 1.7 及以前)

  • ConcurrentHashMap 使用了 分段锁(Segment) 技术,将整个哈希表分为多个段(默认16段)。
  • 每个段有独立的锁,get() 方法只需要找到对应段,直接读取,无需加锁。
  • 只有 put()remove() 等修改操作才会对段加锁。

2. 基于 CAS 和链表/红黑树 (JDK 1.8 及以后)

  • JDK 1.8 改进了 ConcurrentHashMap,摒弃了分段锁,采用了 CAS(Compare and Swap)synchronized 结合的方式。
  • get() 方法直接通过 volatile 修饰的 Node 数组 查找键值对,确保读取操作是可见的,无需加锁。
  • 数据结构变为 数组 + 链表 + 红黑树,提高了查询效率。

为什么修改操作需要加锁?

  • 修改操作(如 put()remove())涉及到哈希表的结构变化,如 扩容节点插入链表/树修改,需要锁或 CAS 来保证线程安全。
  • 主要技术:
    • CAS: 用于节点插入,确保无锁下的原子性。
    • synchronized: 用于链表或红黑树节点的修改,避免冲突。

示例代码: 无锁 get() 操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapExample {
public static void main(String[] args) {
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();

// 放入键值对
map.put("key1", "value1");
map.put("key2", "value2");

// 无锁获取操作
String value = map.get("key1");
System.out.println("获取 key1 的值: " + value); // 输出: value1
}
}

总结:

  • ConcurrentHashMap.get() 在 JDK 1.8 中是 无锁的,因为使用了 volatileCAS 保证了可见性和一致性。
  • 修改操作如 put()remove() 则需要部分加锁,确保哈希表结构在高并发环境下的完整性。
  • 因此,get() 方法在 多线程环境中无需加锁,高效且线程安全。

问:Hashtable 是怎么加锁的 ?

Hashtable 是 Java 中一种 线程安全的 Map 实现,通过对 整个方法加锁 来保证线程安全。

加锁机制

  1. 同步方法 (synchronized 修饰)
    • Hashtable 的几乎所有方法(如 put()get()remove() 等)都用 synchronized 修饰,对方法级别加锁
    • 这意味着每次只能有一个线程执行这些方法,其他线程必须等待锁释放。

示例代码 (源码片段)

1. put() 方法加锁示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public synchronized V put(K key, V value) {
if (key == null || value == null) {
throw new NullPointerException();
}
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % table.length;

for (Entry<K, V> e = table[index]; e != null; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
V oldValue = e.value;
e.value = value;
return oldValue;
}
}

addEntry(hash, key, value, index);
return null;
}

2. get() 方法加锁示例

1
2
3
4
5
6
7
8
9
10
11
public synchronized V get(Object key) {
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % table.length;

for (Entry<K, V> e = table[index]; e != null; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return e.value;
}
}
return null;
}

Hashtable 加锁的缺点

  1. 粒度过粗:
    • 锁定整个方法,导致多个线程无法同时访问不同的键,并发性能较低
  2. 阻塞操作:
    • 如果一个线程持有锁,其他线程 必须阻塞等待,可能导致长时间等待。
  3. 锁竞争激烈:
    • 在高并发环境下,多个线程频繁操作时,锁竞争激烈,性能下降明显。

ConcurrentHashMap 的比较

特性 Hashtable ConcurrentHashMap
加锁机制 方法级别加锁 (synchronized) 锁分段 (JDK 1.7) / CAS + 锁 (JDK 1.8)
线程安全
性能 低,在高并发下表现较差 高,在高并发下表现出色
锁粒度 整个方法 锁分段 / CAS 操作
扩展性 差,不适用于高并发场景 好,适用于高并发环境

总结:

  • Hashtable 使用 方法级别的 synchronized 实现线程安全。
  • 在多线程场景下,Hashtable 的性能较差,锁粒度过粗,锁竞争激烈。
  • 在高并发环境中,推荐使用 **ConcurrentHashMap**,其更细粒度的锁机制能够显著提高性能。

三. 内存模型

问:Java内存模型?为什么要有工作内存和主内存?

为什么要有工作内存和主内存:计算机随着技术发展CPU与内存速度差距越来越大,于是出现高速缓存技术存放CPU常用数据,其速度与CPU接近,多核多线程每个核都有自己的缓存区,所以面临着缓存一致性问题。Java内存模型主要为了解决这个问题。

所有的实例域、静态域和数组元素都存储在堆内存中,堆内存在线程之间共享。可以将它们看作共享变量。而局部变量,方法定义参数和异常处理器参数这些则不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。

Java内存模型定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM(Java内存模型)的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。

即三个规则:

  • 所有的数据都在主内存中。
  • 每个线程都保留一份共享变量的副本。线程对变量的所有操作都必须在这个副本内存中进行,而不能直接读写主内存。
  • 不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。

Java 内存模型 (Java Memory Model, JMM)

Java 内存模型定义了 多线程环境下变量的可见性和有序性规则,规范了线程与内存之间的数据交互,确保 线程安全内存一致性

为什么要有工作内存和主内存?

在多线程环境下,工作内存(线程本地内存)主内存(共享内存) 的设计是为了解决以下两个核心问题:

1. 解决 CPU 缓存与内存的不一致性

  • 现代计算机中,CPU 缓存主内存 之间存在较大的访问速度差异。
  • 使用 工作内存(CPU 缓存) 提高访问速度,同时通过 主内存 保证数据的一致性。

2. 保证线程间的数据隔离和共享

  • 工作内存: 每个线程有独立的内存区域,存储从主内存中加载的变量副本,避免线程间干扰。
  • 主内存: 所有线程共享的内存区域,存储全局变量和对象。

Java 内存模型结构

1
2
3
4
5
6
7
8
9
    +--------------------+
| 主内存 | (共享内存,存储所有对象实例)
+--------------------+
^ ^
| |
+----------+ +----------+
| 工作内存 | | 工作内存 |
| (线程A) | | (线程B) |
+----------+ +----------+

内存交互操作 (JVM 规范定义的8种操作)

  1. lock(锁定): 变量在主内存中标记为独占状态。
  2. unlock(解锁): 解除主内存中变量的锁定状态。
  3. read(读取): 从主内存读取变量值到工作内存。
  4. load(加载): 把从主内存读取到的值存入工作内存。
  5. use(使用): 从工作内存读取变量值用于计算。
  6. assign(赋值): 把值分配给工作内存中的变量。
  7. store(存储): 把工作内存的变量值传送到主内存。
  8. write(写入): 把存储的值更新到主内存。

关键问题: 为什么要有这两个内存?

1. 提高性能 (缓存机制)

  • 主内存访问速度较慢,工作内存提高了 CPU 对内存的访问速度。

2. 支持多线程并发

  • 工作内存为每个线程提供独立的变量副本,避免了线程间数据冲突。

3. 确保数据一致性 (内存可见性)

  • JMM 通过 内存屏障(Memory Barriers)volatile 关键字,确保线程间数据的最新可见性。

内存模型中的核心问题

1. 可见性:

  • 一个线程修改的变量,其他线程能否立即看到?
  • 解决方案: volatilesynchronizedfinal 等。

2. 有序性:

  • 指令执行的顺序是否与代码顺序一致?
  • 解决方案: 内存屏障、指令重排序规则、synchronizedvolatile

3. 原子性:

  • 操作是否不可分割,线程间是否能同时执行?
  • 解决方案: synchronizedLockAtomic 类等。

示例: 可见性问题示例 (未加锁)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Example {
private static boolean stop = false;

public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (!stop) { // 缓存读取,可能无法看到最新值
}
System.out.println("线程结束");
}).start();

Thread.sleep(1000); // 主线程睡眠
stop = true; // 修改共享变量
System.out.println("主线程修改 stop = true");
}
}

解决方案: 使用 volatile 关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Example {
private static volatile boolean stop = false;

public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (!stop) {
}
System.out.println("线程结束");
}).start();

Thread.sleep(1000);
stop = true; // 保证主线程修改立即可见
System.out.println("主线程修改 stop = true");
}
}

总结:

  • 主内存: 存储共享变量,所有线程可见。
  • 工作内存: 存储线程私有变量的副本,提升性能,避免频繁访问主内存。

JMM 通过内存屏障、锁机制和 volatile 等关键字,确保多线程环境下的 可见性有序性原子性,实现高效、安全的并发编程。