java线程相关八股

Java线程与并发核心知识点

线程


线程的创建方式有哪些?

1. 继承 Thread 类

✅实现方式

  • 创建一个类继承 java.lang.Thread 类。
  • 重写其 run() 方法,定义线程要执行的任务。
  • 创建该类的实例并调用 start() 方法启动线程。
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("线程执行: " + Thread.currentThread().getName());
    }
}

// 使用
MyThread t = new MyThread();
t.start(); // 启动线程

2. 实现 Runnable 接口

✅ 实现方式

  • 创建一个类实现 java.lang.Runnable 接口。
  • 实现 run() 方法,定义任务逻辑。
  • 将 Runnable 实例作为参数传递给 Thread 构造器,创建线程对象并启动。
class MyTask implements Runnable {
    @Override
    public void run() {
        System.out.println("任务执行: " + Thread.currentThread().getName());
    }
}

// 使用
Thread t = new Thread(new MyTask());
t.start();

3、实现 Callable 接口 + FutureTask

✅ 实现方式

  • 创建一个类实现 java.util.concurrent.Callable<V> 接口。
  • 实现 call() 方法,该方法可以有返回值,且可以抛出异常
  • 将 Callable 实例包装成 FutureTask
  • 将 FutureTask 作为 Thread 的参数启动线程。
  • 通过 FutureTask.get() 获取返回值(阻塞等待)。
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

class MyTask implements Callable<String> {
    @Override
    public String call() throws Exception {
        Thread.sleep(1000);
        return "Hello from Callable";
    }
}

// 使用
Callable<String> task = new MyTask();
FutureTask<String> futureTask = new FutureTask<>(task);
Thread t = new Thread(futureTask);
t.start();

// 获取结果(阻塞)
String result = futureTask.get(); // "Hello from Callable"
System.out.println(result);

4. 使用线程池(ExecutorService

✅ 实现方式

  • 使用 java.util.concurrent.Executors 工具类创建线程池。
  • 将 Runnable 或 Callable 任务提交给线程池执行。
  • 线程池负责管理线程的生命周期,避免频繁创建和销毁线程
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

ExecutorService executor = Executors.newFixedThreadPool(2);

// 提交 Runnable 任务
executor.submit(() -> {
    System.out.println("线程池任务执行: " + Thread.currentThread().getName());
});

// 提交 Callable 任务
Future<String> future = executor.submit(() -> "Result from pool");
String result = future.get();

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

四种方式对比总结

方式是否有返回值是否可抛异常是否推荐适用场景
继承 Thread⚠️ 不推荐简单演示
实现 Runnable✅ 推荐一般多线程任务
实现 Callable + FutureTask✅ 推荐需要返回结果
线程池(ExecutorService)✅/❌✅/❌✅✅ 强烈推荐生产环境、高并发

Java线程的状态有哪些?

Java 线程的六种状态(附代码详解)_java 线程状态-优快云博客

在Java中,线程的状态(Thread State)由 java.lang.Thread.State 枚举类定义,共有 6种状态

状态触发条件
NEWnew 出来但未 start
RUNNABLE正在运行或就绪
BLOCKED等待进入 synchronized
WAITINGwait/join 无超时
TIMED_WAITINGsleep/wait 有超时
TERMINATEDrun 方法结束

1. NEW(新建)

  • 线程被创建,但尚未调用 start() 方法
  • 此时线程对象已经存在,但还没有被JVM调度执行。

2. RUNNABLE(可运行)

  • 线程已经启动,正在JVM中执行,或者正在等待操作系统资源(如CPU时间片)
  • 注意:RUNNABLE 状态包括了操作系统层面的“就绪”和“运行”两种状态。
  • 并不意味着线程正在运行,只是说它可以运行,等待CPU调度。

3. BLOCKED(阻塞)

  • 线程试图获取一个对象的监视器锁(monitor lock),但该锁被其他线程持有。
  • 常见于 synchronized 代码块或方法的竞争。
  • 例如:线程A持有锁,线程B尝试进入同步代码块 → B进入 BLOCKED 状态。

4. WAITING(无限期等待)

  • 线程无限期等待另一个线程执行特定操作
  • 进入方式:
    • Object.wait()
    • Thread.join()
    • LockSupport.park()
  • 退出方式:只能由其他线程显式唤醒(如 notify()notifyAll())或被中断。

5. TIMED_WAITING(限期等待)

  • 线程在指定时间内等待,时间到后会自动恢复。
  • 进入方式:
    • Thread.sleep(long millis)
    • Object.wait(long timeout)
    • Thread.join(long millis)
    • LockSupport.parkNanos()parkUntil()
  • 不需要其他线程唤醒,超时后自动进入可运行状态。

6. TERMINATED(终止)

  • 线程的 run() 方法已经执行完毕,或者因异常退出。
  • 线程生命周期结束,无法再次启动

如何停止一个线程的运行?

sleep 和 wait的区别是什么?

对比项sleep()wait()
所属类ThreadObject
释放锁❌ 否✅ 是
使用环境任意位置必须在同步块内(持有锁)
唤醒方式超时自动恢复 或 interrupt()notify() / notifyAll() 或 interrupt()
主要用途延时、控制节奏线程协作、通信

sleep() 是“我先睡会儿,但我不放手里的资源”,而 wait() 是“我等你通知,所以我先把资源让出来”。两者用途完全不同,不能互相替代。

1. 所属类不同(根本区别)

sleep()Thread 类的静态方法
wait()Object 类的方法(所有对象都可调用)

2. 是否释放锁(最关键区别)

  • sleep():线程虽然休眠,但仍持有锁,其他线程被阻塞。
  • wait():线程释放锁,允许其他线程竞争该锁,实现线程协作。
synchronized (obj) {
    // sleep 不释放锁
    Thread.sleep(5000); // 其他线程无法进入 synchronized(obj) 块
}

synchronized (obj) {
    obj.wait(); // 当前线程释放 obj 的锁
    // 其他线程可以获取 obj 的锁,进入 synchronized 块
}

3. 使用环境不同

  • sleep()    任何地方都可以调用
  • wait()    必须在 synchronized 块或方法中调用
// ❌ 错误:没有 synchronized
obj.wait(); // 抛出 IllegalMonitorStateException

// ✅ 正确
synchronized (obj) {
    obj.wait();
}

4. 唤醒方式不同

sleep()时间到后自动恢复,或被中断(interrupt()
wait()必须由其他线程显式唤醒:

  • notify():唤醒一个等待线程
  • notifyAll():唤醒所有等待线程
  • 或被中断 |

wait() 是一种线程间协作机制,而 sleep() 是单纯的延时。

5. 所属机制不同

sleep()线程控制工具,用于控制执行节奏
wait()线程通信机制,用于实现生产者-消费者、等待-通知等模式

sleep会释放cpu吗?

是的,Thread.sleep() 会释放CPU资源,但不会释放对象锁。

当一个线程调用 Thread.sleep(milliseconds) 时,它会:

  1. 进入阻塞状态(TIMED_WAITING)

    • 线程从 RUNNABLE 状态变为 TIMED_WAITING 状态。
    • 此时线程不再参与CPU时间片的竞争
  2. 释放CPU使用权

    • 操作系统调度器会将该线程从运行队列中移除。
    • CPU可以调度其他就绪状态的线程执行,提高了CPU的利用率
    • 这正是 sleep() 实现“延时”或“让出CPU”的核心机制。
  3. 不释放对象监视器锁(Monitor Lock)

    • 如果 sleep() 是在 synchronized 块或方法中调用的,线程仍然持有该锁
    • 其他线程无法进入同一对象的同步代码块,即使当前线程在“睡觉”。

blocked和waiting有啥区别

对比项BLOCKED(阻塞)WAITING(等待)
触发原因等待获取对象监视器锁(synchronized等待其他线程显式唤醒(wait/join/park
是否释放锁❌ 不持有锁(正在争抢)✅ 已释放锁
唤醒方式锁被释放后自动竞争必须由其他线程调用 notify() / notifyAll()
所属机制同步控制线程间通信
进入方法进入 synchronized 块/方法Object.wait()Thread.join()LockSupport.park()

BLOCKED 状态(阻塞状态):线程试图进入一个由 synchronized 保护的代码块或方法,但该对象的锁(monitor)被其他线程持有。

  • 特点
    • 线程在锁的入口处排队等待
    • 不持有锁,但正在激烈竞争
    • 属于被动阻塞,由JVM同步机制自动触发。

WAITING 状态(无限期等待):线程主动调用 wait()join() 或 park(),进入无限期等待,直到被其他线程显式唤醒。

  • 特点
    • 线程主动放弃执行权和锁
    • 进入“安静等待”状态,不参与锁竞争。
    • 必须由其他线程通过 notify() 或 notifyAll() 唤醒。
    • 线程协作的基础,用于实现生产者-消费者等模式。

wait 状态下的线程如何进行恢复到 running 状态?

当一个线程处于 WAITING 状态时,它不会像 sleep() 那样自动恢复,而是需要外部显式唤醒才能重新进入 RUNNABLE 状态。

显式唤醒notify() / notifyAll()✅ 推荐
中断唤醒thread.interrupt()✅ 推荐(用于取消任务

1、被其他线程显式唤醒(推荐方式)

  • 调用 notify() 或 notifyAll() 方法。
  • 这些方法必须在同一个对象的同步块或方法中调用,且由持有该对象锁的线程执行。
final Object lock = new Object();

// 等待线程
Thread waiter = new Thread(() -> {
    synchronized (lock) {
        try {
            System.out.println("线程进入等待状态...");
            lock.wait(); // 当前线程释放锁,进入 WAITING 状态
            System.out.println("线程被唤醒,重新获取锁,继续执行");
        } catch (InterruptedException e) {
            System.out.println("线程被中断");
        }
    }
});

// 唤醒线程
Thread notifier = new Thread(() -> {
    synchronized (lock) {
        System.out.println("唤醒等待线程");
        lock.notify(); // 唤醒一个正在等待 lock 的线程
        // 或 lock.notifyAll(); 唤醒所有等待线程
    }
});

waiter.start();
Thread.sleep(1000); // 确保 waiter 先运行
notifier.start();

2. 被中断(Interrupt)

  • 其他线程调用该等待线程的 interrupt() 方法。
  • 会抛出 InterruptedException,并自动退出 WAITING 状态,进入 RUNNABLE 状态以处理异常。
Thread waiter = new Thread(() -> {
    synchronized (lock) {
        try {
            lock.wait();
        } catch (InterruptedException e) {
            System.out.println("线程被中断,从 wait 中唤醒");
            Thread.currentThread().interrupt(); // 恢复中断状态
        }
    }
});

waiter.start();

Thread.sleep(2000);
waiter.interrupt(); // 中断线程,使其从 wait 中醒来

notify 和 notifyAll 的区别?

对比项notify()notifyAll()
唤醒数量随机唤醒一个等待线程唤醒所有等待该对象的线程
唤醒策略JVM 随机选择(通常为等待时间最长的)全部唤醒
资源竞争较少线程竞争锁多个线程同时竞争锁(“惊群效应”)
适用场景只需一个线程处理任务(如连接池)多个线程可能满足条件(如生产者-消费者)
安全性可能遗漏线程(信号丢失)更安全,避免遗漏

notify:唤醒一个线程,其他线程依然处于wait的等待唤醒状态,如果被唤醒的线程结束时没调用notify,其他线程就永远没人去唤醒,只能等待超时,或者被中断

notifyAll:所有线程退出wait的状态,开始竞争锁,但只有一个线程能抢到,这个线程执行完后,其他线程又会有一个幸运儿脱颖而出得到锁

不同的线程之间如何通信?

在Java中,线程之间的通信是并发编程的核心。由于线程拥有独立的栈空间,但共享堆内存和方法区,因此线程通信的本质是通过共享内存协作机制传递信息协调执行

方式适用场景
synchronized + wait/notify基础学习、简单协作
ReentrantLock + Condition精确唤醒、复杂条件
BlockingQueue生产者-消费者、任务队列
volatile 变量状态通知、标志位
CountDownLatch等待多个任务完成
CyclicBarrier多线程同步到达点

 1、基于 synchronized + wait() / notify() / notifyAll()

这是最基础的线程通信机制,基于对象监视器(Monitor)实现。

原理:

  • 线程通过 synchronized 获取对象锁。
  • 使用 wait() 主动释放锁并进入等待状态。
  • 其他线程通过 notify() 或 notifyAll() 唤醒等待线程。

典型应用:生产者-消费者模型

public class ProducerConsumer {
    private final Object lock = new Object();
    private Queue<String> queue = new LinkedList<>();
    private int maxSize = 5;

    public void produce(String item) throws InterruptedException {
        synchronized (lock) {
            while (queue.size() == maxSize) {
                System.out.println("队列满,生产者等待...");
                lock.wait(); // 释放锁,等待
            }
            queue.add(item);
            System.out.println("生产: " + item);
            lock.notifyAll(); // 唤醒所有消费者
        }
    }

    public String consume() throws InterruptedException {
        synchronized (lock) {
            while (queue.isEmpty()) {
                System.out.println("队列空,消费者等待...");
                lock.wait();
            }
            String item = queue.poll();
            System.out.println("消费: " + item);
            lock.notifyAll(); // 唤醒所有生产者
            return item;
        }
    }
}

✅ 2、使用 ReentrantLock + Condition

Conditionjava.util.concurrent.locks 包提供的更灵活的等待/通知机制。

原理:

  • ReentrantLock 可以创建多个 Condition 实例。
  • 每个 Condition 对应一个等待队列,实现精确唤醒

示例:精确唤醒生产者和消费者

import java.util.concurrent.locks.*;

public class BoundedBuffer {
    private final Lock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();
    private String[] buffer = new String[5];
    private int count = 0;

    public void put(String item) throws InterruptedException {
        lock.lock();
        try {
            while (count == buffer.length) {
                notFull.await(); // 等待“不满”条件
            }
            buffer[count++] = item;
            notEmpty.signal(); // 只唤醒消费者
        } finally {
            lock.unlock();
        }
    }

    public String take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                notEmpty.await(); // 等待“不空”条件
            }
            String item = buffer[--count];
            notFull.signal(); // 只唤醒生产者
            return item;
        } finally {
            lock.unlock();
        }
    }
}

优点:

  • 支持多个等待队列,可实现精确唤醒(避免 notifyAll() 的“惊群效应”)。
  • 更灵活的锁控制(可中断、可超时)。

✅ 3、使用阻塞队列(BlockingQueue)

java.util.concurrent 提供了线程安全的阻塞队列,是生产者-消费者的最佳实践

常见实现:

  • ArrayBlockingQueue:有界队列
  • LinkedBlockingQueue:无界/有界队列
  • SynchronousQueue:不存储元素的同步队列
BlockingQueue<String> queue = new ArrayBlockingQueue<>(5);

// 生产者
new Thread(() -> {
    try {
        queue.put("item"); // 队列满时阻塞
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}).start();

// 消费者
new Thread(() -> {
    try {
        String item = queue.take(); // 队列空时阻塞
        System.out.println("消费: " + item);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}).start();

优点:

  • 无需手动同步,线程安全。
  • 语义清晰,代码简洁。
  • 高性能,推荐在生产环境使用。

✅ 4、使用 volatile 变量(状态标志)

适用于简单的线程通知场景,如控制线程停止。

private volatile boolean running = true;

new Thread(() -> {
    while (running) {
        // 执行任务
    }
    System.out.println("线程停止");
}).start();

// 外部控制
running = false; // 通知线程停止

优点:

  • 轻量级,适合状态通知。
  • 利用 volatile 的可见性保证。

✅ 5、使用 CountDownLatch(倒计时门闩)

允许一个或多个线程等待其他线程完成操作。

示例:主线程等待多个子线程完成

CountDownLatch latch = new CountDownLatch(3);

for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        // 执行任务
        latch.countDown(); // 计数减1
    }).start();
}

latch.await(); // 主线程阻塞,直到计数为0
System.out.println("所有子线程完成");

6、使用 CyclicBarrier(循环栅栏)

多个线程互相等待,直到都到达某个屏障点。

示例:多线程并行计算

CyclicBarrier barrier = new CyclicBarrier(3, () -> {
    System.out.println("所有线程已到达,开始汇总");
});

for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        // 计算任务
        barrier.await(); // 等待其他线程
    }).start();
}

线程池


线程池的底层工作原理

线程池内部是通过队列+线程实现的,当我们利用线程池执行任务时:

  1. 如果此时线程池中的线程数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
  2. 如果此时线程池中的线程数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列。
  3. 如果此时线程池中的线程数量大于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。
  4. 如果此时线程池中的线程数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。
  5. 当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数

线程池的核心参数?

  • 任务队列:用于缓存等待执行的任务。队列的大小决定了能缓存多少个任务,而不会直接创建线程来执行任务。
  • 核心线程数(corePoolSize):线程池在没有任务时,仍会保持这么多个线程处于活动状态,随时准备执行任务。
  • 最大线程数(maximumPoolSize):线程池可以创建的最大线程数,超过这个值的任务会进入等待队列,或者根据拒绝策略处理。
  • keepAliveTime:当线程池中线程的数量大于corePoolSize,并且某个线程的空闲时间超过了keepAliveTime,那么这个线程就会被销毁。
  • unit:就是keepAliveTime时间的单位。
  • threadFactory:线程工厂。可以用来给线程取名字等等
  • handler:拒绝策略。当一个新任务交给线程池,如果此时线程池中有空闲的线程,就会直接执行,如果没有空闲的线程,就会将该任务加入到阻塞队列中,如果阻塞队列满了,就会创建一个新线程,从阻塞队列头部取出一个任务来执行,并将新任务加入到阻塞队列末尾。如果当前线程池中线程的数量等于maximumPoolSize,就不会创建新线程,就会去执行拒绝策略

如果线程池在任务到来时首先创建最大线程数,而不使用队列缓存任务,那么无论任务数量如何,都会立刻创建新线程,这不仅没有利用队列的缓冲作用,还可能在瞬间创建大量的线程,造成系统资源浪费。

线程池为什么是先添加列队而不是先创建最大线程?

线程池采用先添加到队列而不是先创建最大线程的设计,主要是为了提高资源利用率

减少线程创建的开销,避免系统过度消耗资源

  • 线程的创建、销毁和上下文切换是有开销的。每次创建一个线程时,JVM 都需要分配内存并进行初始化,而销毁线程时又需要释放资源,这个过程本身就会消耗时间和系统资源。
  • 如果线程池在任务到来时直接创建最大线程,意味着即使任务数量很少,线程池也会创建大量的线程,这不仅增加了系统开销,还可能导致内存不足等问题。

控制线程数目,避免资源过度消耗

  • 如果线程池直接创建最大线程,系统的资源(如 CPU、内存)可能会因为线程数过多而受到压垮,尤其是在大量任务涌入时,系统可能会过度创建线程,导致线程争用,使得系统性能急剧下降。
  • 通过先将任务放入队列,可以限制同时活跃的线程数目,并确保不会因为任务过多而瞬间消耗掉系统的所有资源。

线程池工作队列满了有哪些拒接策略?

策略行为是否丢弃任务是否抛异常适用场景
AbortPolicy抛出异常❌ 不丢弃(但拒绝)✅ 是默认策略,快速失败
CallerRunsPolicy调用者线程执行❌ 不丢弃❌ 否限流、保护线程池
DiscardPolicy静默丢弃✅ 丢弃❌ 否可容忍丢失的任务
DiscardOldestPolicy丢弃最老任务✅ 丢弃(老任务)❌ 否优先处理新任务
1. AbortPolicy(默认策略)
  • 行为直接抛出异常 RejectedExecutionException
  • 特点
    • 最直接、最安全的策略,防止系统在过载时雪崩。
    • 让调用者感知到系统压力,可进行降级或重试处理。
  • 适用场景
    • 对数据完整性要求高,不允许任务丢失。
    • 系统希望快速失败(fail-fast)以保护自身。
2. CallerRunsPolicy
  • 行为由提交任务的线程(即调用者线程)自己执行该任务
  • 特点
    • 不丢弃任务,但会阻塞调用线程
    • 起到“限流”作用:当线程池过载时,迫使调用者放慢提交速度。
  • 适用场景
    • 可接受短暂延迟,希望任务最终能被执行。
    • 调用线程有空闲时间(如Web服务器的请求线程)。
3. DiscardPolicy
  • 行为静默丢弃新提交的任务,不抛异常。
  • 特点
    • 操作简单,系统不会因异常崩溃。
    • 但任务无任何通知地丢失,可能影响业务逻辑。
  • 适用场景
    • 任务可丢失的场景,如日志采集、监控数据上报。
    • 对系统稳定性要求高于任务完整性。
4. DiscardOldestPolicy
  • 行为丢弃队列中最老的任务(即等待时间最长的任务),然后尝试将新任务提交到队列。
  • 特点
    • 优先保证最新任务的执行。
    • 可能导致老任务被反复丢弃(“饿死”)。
  • 适用场景
    • 任务具有时效性,如实时消息处理、订单推送。
    • 新任务比旧任务更重要。

总结:

  • AbortPolicy,直接抛出一个任务被线程池拒绝的异常。
  • CallerRunsPolicy,使用线程池的调用者所在的线程去执行被拒绝的任务,除非线程池被停止或者线程池的任务队列已有空缺。
  • DiscardPolicy,不做任何处理,静默拒绝提交的任务。
  • DiscardOldestPolicy,抛弃最老的任务,然后执行该任务。
  • 自定义拒绝策略,通过实现接口可以自定义任务拒绝策略。

并发


说说对线程安全的理解

线程安全(Thread Safety)是指多个线程同时访问某个对象时,该对象的状态不会受到线程的干扰,并且不会导致错误的行为或数据不一致。

换句话说,线程安全的程序在多线程环境下运行时,不会因为线程的切换和并发访问导致问题

造成死锁的几个原因:

死锁(Deadlock) 是指两个或多个线程在执行过程中,因争夺资源而互相等待,导致线程无法继续执行的情况。死锁会导致程序的执行陷入无限等待状态,影响系统的稳定性和性能。

死锁的四个必要条件

死锁发生的必要条件包括以下四个:

  1. 互斥条件(Mutual Exclusion):至少有一个资源必须处于非共享模式,即每次只能有一个线程使用该资源。
  2. 占有且等待条件(Hold and Wait):一个线程持有一个资源,同时等待另一个线程持有的资源。
  3. 非抢占条件(No Preemption):资源不能被强制从一个线程中剥夺,必须由线程自行释放。
  4. 循环等待条件(Circular Wait):存在一种线程循环等待的关系,线程 T1 等待 T2,线程 T2 等待 T3,直到线程 Tn 等待 T1,形成一个闭环。

为了避免死锁,通常我们需要打破上述的至少一个条件。

怎么保证多线程安全?

所谓线程安全,指的是多个线程同时访问共享资源时,程序仍能正确地表现出预期行为,不会出现数据竞争、脏读、丢失更新等问题。

场景推荐方案
简单同步方法或代码块synchronized
高并发计数、状态更新AtomicInteger 等原子类
复杂锁控制(超时、中断)ReentrantLock
读多写少的共享数据ReentrantReadWriteLock 或 CopyOnWriteArrayList
Map 高并发读写ConcurrentHashMap
避免共享资源ThreadLocal
状态标志位volatile
数据不变的对象不可变对象

多线程安全问题主要源于以下三个特性

问题含义示例
原子性(Atomicity)一个操作不可中断,要么全部执行,要么都不执行i++ 实际是 读-改-写 三步,非原子
可见性(Visibility)一个线程修改了共享变量,其他线程能立即看到线程A改了变量,线程B可能读到旧值
有序性(Ordering)指令重排序可能导致程序执行顺序与代码顺序不一致初始化对象和赋值可能被重排序

1. 使用 synchronized 关键字

  • 作用:保证原子性 + 可见性 + 有序性(进入和退出同步块时有内存屏障)。
  • 原理:基于对象监视器(Monitor)实现互斥访问。
// 修饰实例方法
public synchronized void add() { count++; }

// 修饰静态方法
public static synchronized void staticAdd() { ... }

// 修饰代码块
synchronized (lock) {
    count++;
}

优点:简单、内置、可重入。
缺点:粒度粗,可能影响性能。

2. 使用 volatile 关键字

volatile关键字用于变量,确保所有线程看到的是该变量的最新值,而不是可能存储在本地寄存器中的副本。

  • 作用:保证可见性有序性(禁止指令重排序),但不保证原子性
  • 适用场景
    • 状态标志位(如 volatile boolean running = true;
    • 双重检查单例模式中的实例字段

3. 使用 java.util.concurrent.atomic 包(原子类)

  • 作用:保证原子性,基于 CAS(Compare-And-Swap)操作。
  • 常用类
    • AtomicIntegerAtomicLong
    • AtomicBoolean
    • AtomicReference
    • AtomicLongArray 等

4. 使用显式锁 ReentrantLock / ReentrantReadWriteLock

  • 作用:替代 synchronized,提供更灵活的锁控制。
  • 特性
    • 可中断(lockInterruptibly()
    • 可设置超时(tryLock(timeout)
    • 公平锁/非公平锁
    • 读写分离(ReentrantReadWriteLock

5. 使用线程安全的容器

避免手动同步,优先使用 java.util.concurrent 包下的线程安全集合。

类型非线程安全线程安全替代
ListArrayListCopyOnWriteArrayList
MapHashMapConcurrentHashMap
QueueLinkedListArrayBlockingQueueLinkedBlockingQueue
SetHashSetCopyOnWriteArraySet

6. 使用 ThreadLocal(线程本地变量)

  • 原理:为每个线程提供独立的变量副本,避免共享。
  • 适用场景
    • 数据库连接、Session 管理
    • 日期格式化(SimpleDateFormat 非线程安全)

7. 使用不可变对象(Immutable Object)

  • 原理:对象创建后状态不可变,天然线程安全。
  • 实现方式
    • 所有字段用 final 修饰
    • 不提供 setter 方法
    • 类本身用 final 修饰(防止被继承修改)

Java中有哪些常用的锁,在什么场景下使用?

类别锁类型所属包/机制
内置锁synchronizedJVM 内置
显式锁ReentrantLockjava.util.concurrent.locks
ReentrantReadWriteLock
StampedLock
原子类(无锁)AtomicInteger 等java.util.concurrent.atomic
容器锁ConcurrentHashMap 分段锁java.util.concurrent

java常用锁详解

1. synchronized(内置锁 / 监视器锁)
  • 原理:基于对象的监视器(Monitor),JVM 自动加锁/解锁。
  • 特点
    • 互斥、可重入、保证原子性、可见性、有序性。
    • 简单易用,无需手动释放。
// 修饰实例方法
public synchronized void method() { ... }

// 修饰静态方法
public static synchronized void staticMethod() { ... }

// 修饰代码块
synchronized (this) { ... }
2. ReentrantLock(可重入锁)
  • 原理java.util.concurrent.locks.Lock 接口的实现,基于 AQS(AbstractQueuedSynchronizer)。
  • 特点
    • 可重入、互斥。
    • 支持可中断lockInterruptibly())、超时tryLock(timeout))、公平锁/非公平锁
    • 必须手动 unlock(),建议用 try-finally
private final ReentrantLock lock = new ReentrantLock();

public void method() {
    lock.lock();
    try {
        // 临界区
    } finally {
        lock.unlock();
    }
}

适用场景

  • 需要锁超时避免死锁。
  • 需要响应中断(如取消长时间操作)。
  • 高并发下希望更灵活的锁控制。

synchronized 对比

  • 功能更强大,但代码更复杂。
  • 在高竞争下性能可能优于 synchronized(尤其在Java 6+优化后差距缩小)。
3. ReentrantReadWriteLock(读写锁)
  • 原理:维护一对锁 —— 读锁(共享) 和 写锁(互斥)
  • 特点
    • 读读不互斥:多个线程可同时读。
    • 读写互斥:写时不能读,读时不能写。
    • 写写互斥:只能一个线程写。
    • 支持锁降级(写锁 → 读锁)。
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();

public String getData() {
    readLock.lock();
    try {
        return data;
    } finally {
        readLock.unlock();
    }
}

public void setData(String data) {
    writeLock.lock();
    try {
        this.data = data;
    } finally {
        writeLock.unlock();
    }
}

适用场景

  • 读多写少的场景,如缓存、配置中心、数据库查询缓存。
  • 提高并发读性能。

⚠️ 注意

  • 写锁饥饿问题(大量读线程可能阻塞写线程)。
  • 不支持锁升级(不能从读锁升级为写锁)。
4. StampedLock(Java 8 引入)
  • 原理:比 ReentrantReadWriteLock 更高效的读写锁,使用版本戳(stamp) 机制。
  • 三种模式
    1. 写锁writeLock()):互斥。
    2. 悲观读锁readLock()):类似 ReentrantReadWriteLock 的读锁。
    3. 乐观读tryOptimisticRead()):不加锁,最后通过 validate(stamp) 检查是否被修改。

示例(乐观读):

private final StampedLock stampedLock = new StampedLock();
private double x, y;

public double distanceFromOrigin() {
    long stamp = stampedLock.tryOptimisticRead();
    double currentX = x, currentY = y;
    if (!stampedLock.validate(stamp)) {
        // 被修改,升级为悲观读
        stamp = stampedLock.readLock();
        try {
            currentX = x;
            currentY = y;
        } finally {
            stampedLock.unlockRead(stamp);
        }
    }
    return Math.sqrt(currentX * currentX + currentY * currentY);
}

适用场景

  • 读操作非常频繁,且写操作极少
  • 对性能要求极高,希望减少读锁开销。

⚠️ 注意

  • 不能使用 synchronized 代码块中。
  • 编程复杂度较高。
Atomic 类(CAS 无锁机制)
  • 如 AtomicIntegerAtomicReference
  • 基于 CAS(Compare-And-Swap) 实现,无锁并发。
  • 适用于简单共享变量的原子操作。

场景:计数器、状态标志、高并发自增。

ConcurrentHashMap(分段锁 / CAS)
  • JDK 1.8 后使用 CAS + synchronized 实现。
  • 细粒度锁,不同桶可并发操作。
  • 高性能并发 Map。

场景:缓存、高频读写的 Map。

介绍一下AQS

AQS = 状态管理(state) + 线程排队(CLH Queue) + 模板方法模式

AQS 的核心是用一个 int 类型的 state 变量 来表示同步状态(如锁的持有次数、信号量的许可数、倒计时的剩余数),并通过一个 FIFO 等待队列(CLH 队列的变种) 来管理竞争资源失败的线程。

AQS最核心的就是三大部分:

  • 状态:state;
  • 控制线程抢锁和配合的FIFO队列(双向链表);
  • 期望协作工具类去实现的获取/释放等重要方法(重写)。
1. volatile int state(同步状态)
  • 表示共享资源的状态。
  • 通过 getState()setState(int)compareAndSetState(int, int) 操作。
  • 子类根据语义赋予 state 不同含义
    • ReentrantLockstate 表示锁的重入次数。
    • Semaphorestate 表示剩余许可数。
    • CountDownLatchstate 表示倒计时计数。
2. FIFO 等待队列(CLH Queue)
  • AQS 维护一个双向链表,称为 Sync Queue,用于存放竞争失败的线程
  • 每个节点(Node)代表一个线程,包含线程引用、等待状态(waitStatus)、前后指针。
  • 线程在获取资源失败后,会被封装成 Node 加入队列尾部,并阻塞
  • 当资源释放时,唤醒队列中的下一个线程
3. 模板方法模式

AQS 定义了顶层的同步框架,但将具体的获取/释放逻辑交给子类通过钩子方法实现:

钩子方法说明子类需重写
tryAcquire(int)独占式获取同步状态
tryRelease(int)独占式释放同步状态
tryAcquireShared(int)共享式获取同步状态
tryReleaseShared(int)共享式释放同步状态
isHeldExclusively()当前线程是否独占资源✅(如 ReentrantLock

AQS 的 acquire()release() 等公共方法会调用这些钩子方法。

AQS 在常见并发工具中的应用

工具AQS 的使用方式state 含义
ReentrantLock使用 独占模式锁的重入次数
Semaphore使用 共享模式剩余许可数
CountDownLatch使用 共享模式倒计时计数
ReentrantReadWriteLock读锁用共享模式,写锁用独占模式读锁:读线程数;写锁:重入次数
ThreadPoolExecutor.WorkerWorker 继承 AQS,实现独占表示线程是否正在执行任务

java中的CAS是什么

Java中的CAS(Compare and Swap)是一种实现并发编程的底层机制,它通过硬件级别的原子指令来保证操作的线程安全,是java.util.concurrent.atomic包下原子类(如AtomicIntegerAtomicLong等)的核心基础。

它是怎么工作的?

CAS操作包含三个值:

  • V:内存位置(比如一个变量的地址)
  • A:预期值(我期望当前V的值是A)
  • B:新值(我要把V的值改成B)

执行时:

如果V的当前值等于A,就把V更新为B,并返回true;
如果不等于A,说明被别的线程改过了,就不更新,返回false。

这个过程是原子的,不会被其他线程打断。

AtomicInteger atomicInt = new AtomicInteger(0);

// 相当于 atomicInt.incrementAndGet()
int oldValue = atomicInt.get();
while (!atomicInt.compareAndSet(oldValue, oldValue + 1)) {
    oldValue = atomicInt.get(); // 失败就重试
}

优点

  • 无锁:不阻塞线程,避免了传统synchronized带来的上下文切换开销。
  • 高性能:在并发不激烈时,效率非常高。

缺点与问题

  1. ABA问题
    值从A变成B又变回A,CAS会认为没变过。
    解决:用AtomicStampedReference加上版本号。

  2. 自旋开销
    如果竞争激烈,线程会不断重试(自旋),浪费CPU。
    解决:限制重试次数或改用锁。

  3. 只能保证单个变量的原子性
    多个变量的复合操作无法用一次CAS保证。

总结

Java中的CAS是实现乐观锁无锁并发的关键技术,让AtomicInteger这类类能在不加锁的情况下安全地进行并发操作,是高并发编程的基石之一。

java05(类、泛型、JVM、线程)---java八股_java 类加载器有哪些-优快云博客

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值