线程
线程的创建方式有哪些?
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)时,它会:
进入阻塞状态(TIMED_WAITING)
- 线程从
RUNNABLE状态变为TIMED_WAITING状态。- 此时线程不再参与CPU时间片的竞争。
释放CPU使用权
- 操作系统调度器会将该线程从运行队列中移除。
- CPU可以调度其他就绪状态的线程执行,提高了CPU的利用率。
- 这正是
sleep()实现“延时”或“让出CPU”的核心机制。不释放对象监视器锁(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
Condition是java.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(); }
线程池
线程池的底层工作原理
线程池内部是通过队列+线程实现的,当我们利用线程池执行任务时:
- 如果此时线程池中的线程数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
- 如果此时线程池中的线程数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列。
- 如果此时线程池中的线程数量大于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。
- 如果此时线程池中的线程数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。
- 当线程池中的线程数量大于 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) 是指两个或多个线程在执行过程中,因争夺资源而互相等待,导致线程无法继续执行的情况。死锁会导致程序的执行陷入无限等待状态,影响系统的稳定性和性能。
死锁的四个必要条件
死锁发生的必要条件包括以下四个:
- 互斥条件(Mutual Exclusion):至少有一个资源必须处于非共享模式,即每次只能有一个线程使用该资源。
- 占有且等待条件(Hold and Wait):一个线程持有一个资源,同时等待另一个线程持有的资源。
- 非抢占条件(No Preemption):资源不能被强制从一个线程中剥夺,必须由线程自行释放。
- 循环等待条件(Circular Wait):存在一种线程循环等待的关系,线程
T1等待T2,线程T2等待T3,直到线程Tn等待T1,形成一个闭环。为了避免死锁,通常我们需要打破上述的至少一个条件。
怎么保证多线程安全?
所谓线程安全,指的是多个线程同时访问共享资源时,程序仍能正确地表现出预期行为,不会出现数据竞争、脏读、丢失更新等问题。
场景 推荐方案 简单同步方法或代码块 synchronized高并发计数、状态更新 AtomicInteger等原子类复杂锁控制(超时、中断) ReentrantLock读多写少的共享数据 ReentrantReadWriteLock或CopyOnWriteArrayListMap 高并发读写 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)操作。- 常用类:
AtomicInteger、AtomicLongAtomicBooleanAtomicReferenceAtomicLongArray等4. 使用显式锁
ReentrantLock/ReentrantReadWriteLock
- 作用:替代
synchronized,提供更灵活的锁控制。- 特性:
- 可中断(
lockInterruptibly())- 可设置超时(
tryLock(timeout))- 公平锁/非公平锁
- 读写分离(
ReentrantReadWriteLock)5. 使用线程安全的容器
避免手动同步,优先使用
java.util.concurrent包下的线程安全集合。
类型 非线程安全 线程安全替代 List ArrayListCopyOnWriteArrayListMap HashMapConcurrentHashMapQueue LinkedListArrayBlockingQueue,LinkedBlockingQueueSet HashSetCopyOnWriteArraySet6. 使用
ThreadLocal(线程本地变量)
- 原理:为每个线程提供独立的变量副本,避免共享。
- 适用场景:
- 数据库连接、Session 管理
- 日期格式化(
SimpleDateFormat非线程安全)7. 使用不可变对象(Immutable Object)
- 原理:对象创建后状态不可变,天然线程安全。
- 实现方式:
- 所有字段用
final修饰- 不提供 setter 方法
- 类本身用
final修饰(防止被继承修改)
Java中有哪些常用的锁,在什么场景下使用?
| 类别 | 锁类型 | 所属包/机制 |
|---|---|---|
| 内置锁 | synchronized | JVM 内置 |
| 显式锁 | ReentrantLock | java.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) 机制。- 三种模式:
- 写锁(
writeLock()):互斥。- 悲观读锁(
readLock()):类似ReentrantReadWriteLock的读锁。- 乐观读(
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 无锁机制)
- 如
AtomicInteger、AtomicReference。- 基于 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不同含义:
ReentrantLock:state表示锁的重入次数。Semaphore:state表示剩余许可数。CountDownLatch:state表示倒计时计数。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包下原子类(如AtomicInteger、AtomicLong等)的核心基础。它是怎么工作的?
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带来的上下文切换开销。- 高性能:在并发不激烈时,效率非常高。
缺点与问题
ABA问题:
值从A变成B又变回A,CAS会认为没变过。
解决:用AtomicStampedReference加上版本号。自旋开销:
如果竞争激烈,线程会不断重试(自旋),浪费CPU。
解决:限制重试次数或改用锁。只能保证单个变量的原子性:
多个变量的复合操作无法用一次CAS保证。总结
Java中的CAS是实现乐观锁和无锁并发的关键技术,让
AtomicInteger这类类能在不加锁的情况下安全地进行并发操作,是高并发编程的基石之一。
Java线程与并发核心知识点


1513

被折叠的 条评论
为什么被折叠?



