AI大模型教程
一起来学习

java基础(五)多线程篇

一、 先搞懂几个基础概念

1. 等待 vs 阻塞:买钢笔的故事

假设小卖铺只剩一支钢笔了。

  • 顾客A 先到,买走了钢笔。

  • 顾客B 后到,也想要钢笔。

这时顾客B有两种选择:

  1. 阻塞 (Blocking): 顾客B就杵在柜台前,啥也不干,一直死等,直到老板进货上架新钢笔他立马买走。(线程占着位置不干活,干等资源)

  2. 等待 (Waiting): 顾客B跟老板说:“钢笔到了叫我一声!”,然后他就回家干别的事去了。等老板进货了,通知他,他再来买。(线程让出位置去做别的事或休眠,等通知唤醒)

关键点: 阻塞通常是系统自动做的(比如抢不到锁),等待通常是程序员主动让线程做的(比如调用 wait())。

2. 用户态 vs 内核态:办事的权限级别

想象一下公司有两个办公区域:

  • 用户态 (User Mode): 普通员工的办公区。在这里,员工(应用程序)能做的事情有限,权限比较低。如果员工想做一些需要更高权限的事情(比如申请特殊设备、发重要通知),就得打报告。

  • 内核态 (Kernel Mode): 公司核心管理层的办公区。这里拥有最高权限,可以访问所有硬件资源(CPU、内存、硬盘、网卡),执行所有指令。

线程切换为啥慢? 当线程需要做一件只有内核才能做的事(比如读写文件、网络通信、创建新线程),它就得从“普通员工区”(用户态)跑到“核心管理层区”(内核态)去申请。这个“跑腿”的过程(上下文切换)是比较耗时的。频繁切换就会降低效率。

图解示意 (想象一下):

[ 用户程序 (线程A, 线程B) ]    [ 操作系统内核 (最高权限) ]
       ^                                       ^
       |                                       |
       |--- 普通操作 (用户态) -------------------|
       |                                       |
       |--- 需要硬件/核心资源 (切换到内核态) ------|

二、 Java线程:创建、状态与管理

1. Java线程 = 操作系统线程?

是的!Java线程 (java.lang.Thread) 在底层是通过调用操作系统的线程API(比如Linux的 pthread_create)来实现的。所以,Java程序创建的线程,本质上就是操作系统的线程,是 1:1 的线程模型。一个Java线程对应一个操作系统线程。

2. 多线程要注意啥?安全第一!

多个顾客(线程)同时操作同一个商品(共享数据),很容易乱套!比如:

  • 数据竞争: 两个收银员同时修改同一个商品的库存,可能导致库存数算错。

  • 可见性问题: 一个收银员改了价格标签,但另一个收银员没看到新标签,还在按旧价格卖。

为了保证线程安全(数据不乱),Java主要从三个方面入手:

  • 原子性 (Atomicity): 一个操作要么全做完,要么一点不做,中间不能被其他线程打断。就像收银员扫码收款,要么扫完收完钱完成交易,要么没扫成,不能扫一半被另一个收银员打断。

    • 工具: synchronized 关键字,java.util.concurrent.atomic 包下的类 (如 AtomicInteger)。

  • 可见性 (Visibility): 一个线程修改了共享变量的值,其他线程能马上看到最新值。就像价格标签改了,所有收银员要立刻知道。

    • 工具: volatile 关键字,synchronized

  • 有序性 (Ordering): 程序执行的顺序要符合我们的预期。编译器或处理器有时会优化指令顺序来提高效率(指令重排序),但在多线程下可能导致奇怪的结果。需要保证某些关键操作的顺序。

    • 工具: volatile (部分有序性),synchronized (完全有序性),Happens-Before 原则。

保证数据一致性方案:

  1. 加锁 (Locking): 最常用。像给收银台加把锁(synchronizedReentrantLock),同一时间只允许一个收银员(线程)操作。简单粗暴有效。

  2. 原子变量 (Atomic Variables): 对单个变量(如计数器)的操作保证原子性 (AtomicInteger 等),性能通常比锁好。

  3. 不可变对象 (Immutable Objects): 数据一旦创建就不能改。大家都只能读,自然安全!(比如 String)。

  4. 线程局部变量 (ThreadLocal): 给每个线程发一个专属小本本(ThreadLocal),数据只写在自己的本本上,互相不干扰。

  5. 版本控制 (乐观锁): 常用于数据库。修改数据前先记下版本号,提交时检查版本号没变才更新(如 CAS 或数据库 version 字段)。

  6. 事务 (Transactions): 数据库层面的保障,一组操作要么全成功,要么全失败回滚(ACID特性)。

3. 创建线程的四种姿势

姿势一:继承 Thread 类 (不推荐常用)
class MyThread extends Thread { // YA33提醒:继承Thread类
    @Override
    public void run() {
        // 线程要执行的代码在这里
        System.out.println("YA33的线程-继承方式运行了!");
    }
}

public static void main(String[] args) {
    MyThread t = new MyThread();
    t.start(); // 启动线程,注意是start()不是run()!
}
  • 优点: 写法简单,在 run() 方法里直接用 this 就能拿到当前线程对象。

  • 缺点: Java是单继承,继承了 Thread 就不能继承别的类了,限制了扩展性。

姿势二:实现 Runnable 接口 (最常用)
class MyRunnable implements Runnable { // YA33提醒:实现Runnable接口
    @Override
    public void run() {
        // 线程要执行的代码在这里
        System.out.println("YA33的线程-实现Runnable运行了!");
    }
}

public static void main(String[] args) {
    Thread t = new Thread(new MyRunnable()); // 把Runnable任务传给Thread
    t.start();
}
  • 优点:

    • 灵活!实现接口不影响继承其他类。

    • 多个线程可以共享同一个 Runnable 对象(比如操作同一份资源),代码和数据分离更清晰。

  • 缺点:run() 方法里获取当前线程需要用 Thread.currentThread()

姿势三:实现 Callable 接口 + FutureTask (能返回结果)
import java.util.concurrent.*;

class MyCallable implements Callable { // YA33提醒:实现Callable
    @Override
    public Integer call() throws Exception {
        // 线程要执行的代码,可以返回结果
        Thread.sleep(1000);
        return 42; // 返回一个整数结果
    }
}

public static void main(String[] args) {
    MyCallable task = new MyCallable();
    FutureTask futureTask = new FutureTask(task); // 包装Callable
    Thread t = new Thread(futureTask); // 传给Thread
    t.start();

    try {
        Integer result = futureTask.get(); // YA33提醒:这里会等待线程执行完拿到结果
        System.out.println("YA33的Callable线程结果: " + result);
    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    }
}
  • 优点:

    • 能返回执行结果 (call() 方法的返回值)。

    • 能抛出异常。

    • 同样具有实现 Runnable 的灵活性优点。

  • 缺点: 获取当前线程也需要用 Thread.currentThread(),使用稍复杂点。

姿势四:使用线程池 (ExecutorService) (生产环境首选)
import java.util.concurrent.*;

class Task implements Runnable { // YA33提醒:还是实现Runnable
    @Override
    public void run() {
        // 任务代码
        System.out.println("YA33的线程池任务运行中...");
    }
}

public static void main(String[] args) {
    // 创建一个固定大小为10的线程池 (YA33常用方式)
    ExecutorService executor = Executors.newFixedThreadPool(10);

    // 提交100个任务给线程池
    for (int i = 0; i 
  • 优点 (YA33强烈推荐):

    • 性能高: 重用已存在的线程,避免频繁创建销毁线程的巨大开销。

    • 管理方便: 有效控制并发线程数量,防止过多线程耗尽系统资源(如内存溢出OOM)。

    • 功能强大: 提供任务提交、结果获取(Future)、定时执行、关闭管理等功能。

  • 缺点:

    • 配置参数需要一定理解(核心线程数、最大线程数、队列类型、拒绝策略等)。

    • 使用不当可能造成死锁、资源耗尽等问题,排查相对复杂。

总结选哪个? 日常开发优先选 姿势二 (Runnable)姿势四 (线程池)。需要返回值或异常处理用 姿势三 (Callable)。姿势一了解即可。

4. 启动与停止线程

  • 启动线程: 必须调用线程对象的 start() 方法!千万别直接调用 run() 方法(那就变成普通方法调用,没有新线程了!)。

    Thread thread1 = new Thread(...);
    Thread thread2 = new Thread(...);
    thread1.start(); // YA33: 正确启动!
    thread2.start();
  • 停止线程 (安全协作式): Java 不推荐用已废弃的 stop() (太暴力,容易导致数据不一致)。正确姿势是:

    1. 使用标志位: 在任务循环中检查一个 volatile 变量。

      public class SafeStop implements Runnable {
          private volatile boolean running = true; // YA33: volatile保证可见性
      
          @Override
          public void run() {
              while (running) { // 检查标志
                  // ... 工作代码 ...
              }
              System.out.println("YA33: 线程安全停止");
          }
          public void stop() {
              running = false; // 外部调用停止
          }
      }
    2. 使用中断 (interrupt()): 调用线程的 interrupt() 方法设置中断标志。任务中需要检查这个标志 (Thread.currentThread().isInterrupted()) 或处理 InterruptedException

      public class InterruptExample implements Runnable {
          @Override
          public void run() {
              while (!Thread.currentThread().isInterrupted()) { // 检查中断标志
                  try {
                      // ... 可能阻塞的代码 (如sleep, wait, join) ...
                      Thread.sleep(1000);
                  } catch (InterruptedException e) {
                      // 阻塞时被中断会抛异常,并且清除中断状态!通常在这里退出
                      System.out.println("YA33: 睡眠中被中断");
                      Thread.currentThread().interrupt(); // 重新设置中断标志 (重要!)
                  }
              }
              System.out.println("YA33: 线程被中断停止");
          }
      }
      // 主线程中
      thread.interrupt(); // 请求中断

      关键点: interrupt() 只是设置一个标志位,不会强制停止线程。线程需要自己检查这个标志并决定如何退出。

5. 线程的六种状态 (生命周期)

Java 线程有 6 种状态 (java.lang.Thread.State):

  1. NEW (新建): 刚创建出来 (new Thread()),还没调用 start() 方法。

  2. RUNNABLE (可运行): 调用 start() 后进入此状态。包含了操作系统层面的【就绪】(等待CPU分配时间片) 和 【运行中】(正在执行) 两种状态。

  3. BLOCKED (阻塞): 线程等待获取一个监视器锁 (Monitor Lock) 而进入的状态。比如,试图进入一个 synchronized 块/方法,但锁被其他线程占用时。被动触发。

  4. WAITING (无限期等待): 线程等待另一个线程执行特定操作(唤醒)。需要其他线程显式唤醒。调用以下方法会进入WAITING:

    • Object.wait() (不配超时)

    • Thread.join() (不配超时)

    • LockSupport.park() (不配超时)

  5. TIMED_WAITING (限期等待): 和 WAITING 类似,但设置了等待时间。时间到了或收到唤醒通知就会退出。调用以下方法会进入TIMED_WAITING:

    • Thread.sleep(long millis)

    • Object.wait(long timeout)

    • Thread.join(long millis)

    • LockSupport.parkNanos(...) / LockSupport.parkUntil(...)

  6. TERMINATED (终止): 线程执行完毕 (run() 方法结束) 或者因异常退出后的状态。生命周期结束。

图解线程状态流转 (简化版):

6. sleep()wait() 的恩怨情仇

特性 sleep() wait()
所属类 Thread 类的静态方法 Object 类的实例方法
锁释放 不会释放持有的任何锁 释放调用它的对象上的锁
使用前提 可在任何地方调用 必须synchronized 块或方法内 (持有锁)
唤醒机制 超时后自动恢复到可运行(RUNNABLE) 需要其他线程调用 notify() / notifyAll() 或超时
设计用途 单纯让当前线程暂停执行指定时间 用于线程间协调,释放锁让其他线程工作

YA33白话解释:

  • sleep(1000):哥们我困了,睡1秒。我占着的收银台(锁)不让,你们(其他线程)别想用,等我睡醒接着干活。

  • wait():老板货没到,我先出去抽根烟(释放收银台锁),你们谁要用收银台先用。老板货到了记得 notify() 我一下!我收到通知就回来(重新进入锁竞争)。

sleep() 会释放 CPU 吗? 是的!调用 Thread.sleep() 时,线程会主动让出 CPU,进入 TIMED_WAITING 状态。CPU 会去执行其他就绪状态的线程。但它不会释放它已经持有的任何锁!

7. BLOCKED vs WAITING:都是等,区别在哪?

  • 触发原因:

    • BLOCKED:线程被动触发。因为想抢一个锁 (synchronized 保护的资源),但锁被别人占着,抢失败了,系统自动把它放进了阻塞队列。

    • WAITING:线程主动触发。程序员在代码里明确调用了 wait(), join() 等方法,让它主动放弃锁并进入等待队列。

  • 唤醒机制:

    • BLOCKED自动唤醒。当它想要的锁被释放了,系统会自动通知所有在等这把锁的线程:“锁有空了!快来抢!”。然后这些线程从 BLOCKED 状态变成 RUNNABLE 状态去竞争锁。

    • WAITING需要显式唤醒。必须由其他线程调用特定的方法 (notify(), notifyAll(), 或者 LockSupport.unpark(对应线程)) 来唤醒它。或者等到设置的超时时间(TIMED_WAITING)。

核心区别两点:

  1. BLOCKED 是被动等锁,WAITING 是主动放弃锁等通知。

  2. BLOCKED 的唤醒是自动的(锁释放时),WAITING 的唤醒必须靠别人 (notify) 或超时。

8. 线程间如何通信?(说说话)

多个线程要协作完成任务,总得交流吧?有几种常见方式:

  1. 共享变量 + 同步 (最基本): 多个线程读写同一个变量。必须用 volatile 或锁 (synchronized) 保证可见性和原子性!

    class SharedVariableExample {
        private static volatile boolean flag = false; // YA33: volatile保证可见性
    
        public static void main(String[] args) {
            // 生产者线程 (设置flag)
            Thread producer = new Thread(() -> {
                try {
                    Thread.sleep(2000); // 模拟工作
                    flag = true; // 修改共享变量
                    System.out.println("YA33-Producer: Flag set true!");
                } catch (InterruptedException e) { ... }
            });
            // 消费者线程 (等待flag)
            Thread consumer = new Thread(() -> {
                while (!flag) { // 等待flag变true (忙等待 - 浪费CPU!)
                    // 可以加Thread.sleep(少量)减少CPU消耗
                }
                System.out.println("YA33-Consumer: Flag is true now!");
            });
            producer.start();
            consumer.start();
        }
    }
    • 缺点:消费者线程的 while 循环 (忙等待) 会浪费 CPU。volatile 只保证可见性,不保证复合操作原子性。

  2. wait() / notify() / notifyAll() (经典等待通知): 基于对象的监视器锁 (synchronized)。这是 Object 类的方法。

    class WaitNotifyExample {
        private static final Object lock = new Object(); // YA33: 共享锁对象
    
        public static void main(String[] args) {
            // 消费者线程 (先启动,等待)
            Thread consumer = new Thread(() -> {
                synchronized (lock) { // 1. 先获取锁
                    System.out.println("YA33-Consumer: 等待老板进货...");
                    try {
                        lock.wait(); // 2. 等待(释放锁)!等老板notify
                    } catch (InterruptedException e) { ... }
                    System.out.println("YA33-Consumer: 老板喊我,买到笔了!");
                }
            });
            // 生产者线程 (后启动,通知)
            Thread producer = new Thread(() -> {
                synchronized (lock) { // 3. 获取锁 (此时消费者在wait释放了锁)
                    System.out.println("YA33-Producer: 正在进货...");
                    try {
                        Thread.sleep(2000); // 模拟进货时间
                    } catch (InterruptedException e) { ... }
                    System.out.println("YA33-Producer: 货到了!通知顾客");
                    lock.notify(); // 4. 通知一个等待的顾客 (消费者)
                    // lock.notifyAll(); // 通知所有等待的顾客
                }
            });
            consumer.start();
            // 稍微等一下让consumer先拿到锁 (实际开发需要更健壮机制)
            try { Thread.sleep(10); } catch (Exception e) {}
            producer.start();
        }
    }
    • 关键点: wait(), notify(), notifyAll() 必须在 synchronized 块内调用! 调用它们的对象必须是你获取锁的那个对象 (lock)。

    • notify() 随机唤醒一个在 lockwait() 的线程。

    • notifyAll() 唤醒所有在 lockwait() 的线程。它们会一起竞争锁。

  3. Lock + Condition (更灵活的等待通知): Java 5+ 引入的 java.util.concurrent.locks 包提供了更强大的锁和条件变量。

    import java.util.concurrent.locks.*;
    
    class LockConditionExample {
        private static final Lock lock = new ReentrantLock(); // YA33: 可重入锁
        private static final Condition condition = lock.newCondition(); // 条件变量
    
        public static void main(String[] args) {
            // 消费者线程 (等待)
            Thread consumer = new Thread(() -> {
                lock.lock(); // 获取锁
                try {
                    System.out.println("YA33-Consumer: 等待条件满足...");
                    condition.await(); // 等待 (类似wait,释放锁)
                    System.out.println("YA33-Consumer: 条件满足,继续干活!");
                } catch (InterruptedException e) { ... } finally {
                    lock.unlock(); // 释放锁 (必须在finally!)
                }
            });
            // 生产者线程 (通知)
            Thread producer = new Thread(() -> {
                lock.lock(); // 获取锁
                try {
                    System.out.println("YA33-Producer: 工作中...");
                    Thread.sleep(2000);
                    System.out.println("YA33-Producer: 工作完成,通知消费者");
                    condition.signal(); // 通知一个等待者 (类似notify)
                    // condition.signalAll(); // 通知所有等待者
                } catch (InterruptedException e) { ... } finally {
                    lock.unlock(); // 释放锁
                }
            });
            consumer.start();
            // 稍微等一下
            try { Thread.sleep(10); } catch (Exception e) {}
            producer.start();
        }
    }
    • 优点: 一个锁可以关联多个 Condition (条件队列),实现更精细的线程分组通知(比如生产者通知消费者,消费者通知打包员)。Lock 本身也比 synchronized 功能多(可中断、超时、公平锁等)。

  4. BlockingQueue (阻塞队列): 线程安全的队列,是生产者-消费者模型的绝佳实现。队列满时生产者阻塞,队列空时消费者阻塞。

    import java.util.concurrent.*;
    
    class BlockingQueueExample {
        private static final BlockingQueue queue = new LinkedBlockingQueue(1); // YA33: 容量1的队列
    
        public static void main(String[] args) {
            // 生产者线程
            Thread producer = new Thread(() -> {
                try {
                    System.out.println("YA33-Producer: 生产商品1");
                    queue.put(1); // 放入队列,如果队列满则阻塞等待
                    System.out.println("YA33-Producer: 商品1已上架");
                } catch (InterruptedException e) { ... }
            });
            // 消费者线程
            Thread consumer = new Thread(() -> {
                try {
                    System.out.println("YA33-Consumer: 等待商品...");
                    int item = queue.take(); // 从队列取,如果队列空则阻塞等待
                    System.out.println("YA33-Consumer: 买到商品 " + item);
                } catch (InterruptedException e) { ... }
            });
            consumer.start(); // 消费者先启动等着
            producer.start();
        }
    }
    • 优点: 使用简单,解耦生产者和消费者,内部实现了线程同步。

  5. 同步工具类 (CountDownLatch, CyclicBarrier, Semaphore): 更高级的协调工具。

    • CountDownLatch (倒计时门闩): 让一个或多个线程等待其他一组线程完成任务。初始化一个计数(N),每个线程完成任务调用 countDown() (计数-1)。调用 await() 的线程阻塞,直到计数减到0。一次性使用。

    • CyclicBarrier (循环栅栏): 让一组线程相互等待,到达一个公共屏障点(barrier)再一起继续执行。初始化指定参与线程数。每个线程调用 await() 表示到达屏障点并阻塞,直到最后一个线程到达屏障点,所有线程被唤醒继续执行。可循环使用。

    • Semaphore (信号量): 控制同时访问特定资源的线程数量。初始化许可数(permits)。线程通过 acquire() 获取许可(许可-1,没许可则阻塞),通过 release() 释放许可(许可+1)。常用于资源池(如数据库连接池)。

总结通信方式:

  • 简单状态传递:volatile + 循环 (慎用忙等待)

  • 等待/通知:wait/notify (基于 synchronized), Condition (基于 Lock)

  • 生产者-消费者:BlockingQueue (首选)

  • 线程集合点/计数器:CountDownLatch, CyclicBarrier

  • 资源数量控制:Semaphore

三、 线程安全与锁的江湖

1. 保证多线程安全的核心武器

  • synchronized 关键字: 内置锁,简单易用。修饰代码块或方法。

    // 同步方法 (锁this实例)
    public synchronized void addMoney() { ... }
    
    // 同步代码块 (锁指定对象)
    public void transfer() {
        synchronized (accountLock) { // YA33: 使用专门锁对象更清晰
            ... // 操作账户
        }
    }
  • volatile 关键字: 保证变量的可见性部分有序性(防止指令重排序)。不能保证原子性! 适合做状态标志位。

    private volatile boolean shutdownRequested = false; // YA33: 关机标志
  • Lock 接口 (如 ReentrantLock):synchronized 更灵活强大。可中断、可超时、公平锁、多个条件变量。

    private final Lock lock = new ReentrantLock(); // YA33: 可重入锁
    public void criticalSection() {
        lock.lock(); // 获取锁
        try {
            ... // 临界区代码
        } finally {
            lock.unlock(); // 必须在finally释放锁!
        }
    }
  • 原子类 (AtomicInteger, AtomicReference 等): 利用 CAS 操作保证对单个变量的操作是原子的。性能通常优于锁。

    private AtomicInteger counter = new AtomicInteger(0); // YA33: 原子计数器
    public void increment() {
        counter.incrementAndGet(); // 原子自增
    }
  • 不可变对象: 对象一旦创建状态就不能改变。线程安全是天然的。String, Integer 等都是不可变对象。

  • 线程局部变量 (ThreadLocal): 每个线程有自己的变量副本,互不干扰。常用于存储用户会话信息、数据库连接(不推荐连接池场景)等。

    private static ThreadLocal currentUser = new ThreadLocal(); // YA33: 当前线程用户

2. synchronized vs ReentrantLock

特性 synchronized ReentrantLock
实现方式 JVM 内置实现 (监视器锁 monitor) 基于 AQS (AbstractQueuedSynchronizer) 在 JDK 实现
锁获取释放 自动获取和释放锁 (进入/退出同步块) 必须手动 lock()unlock() (通常在 try-finally)
可中断 不支持 支持 (lockInterruptibly())
超时获取 不支持 支持 (tryLock(long time, TimeUnit unit))
公平锁 非公平锁 (无法指定) 可选公平锁或非公平锁 (构造参数 true/false)
条件变量 一个锁关联一个隐式条件 (wait/notify) 一个锁可关联多个 Condition
锁绑定 绑定在对象头或 Class 对象上 不绑定特定对象
代码可读性 简洁 相对复杂,需显式管理锁
锁升级 支持 (无锁->偏向锁->轻量级锁->重量级锁) 不支持
性能 (低竞争) 通常更好 稍差
性能 (高竞争) 通常较差 通常更好

YA33 选择建议:

  • 简单场景、代码量少:用 synchronized

  • 需要高级功能(中断、超时、公平锁、多条件)、高竞争性能要求:用 ReentrantLock

  • 生产环境线程池任务:优先考虑 ReentrantLock 或原子类。

3. 什么是可重入锁?

可重入锁是指:同一个线程可以多次获取自己已经持有的锁,而不会被自己阻塞。

想象场景: 你进了自己家大门(获取了锁),可以随意进出家里的各个房间(再次获取同一把锁),不需要出了大门再重新进。synchronizedReentrantLock 都是可重入锁。

实现原理: 内部维护一个计数器(hold count)。第一次获取锁,计数器=1。同一线程再次获取,计数器+1。释放锁时计数器-1。只有当计数器减到0时,锁才真正释放,其他线程才能获取。

4. 锁升级 (synchronized 优化)

为了提高 synchronized 的性能,JVM 会根据锁竞争情况动态升级锁的状态:

  1. 无锁 (Lockless): 初始状态,没有线程竞争。

  2. 偏向锁 (Biased Locking): 假设锁总是由同一个线程获得。这个线程获取锁时,会在对象头和栈帧锁记录里存储线程ID。以后该线程进入只需简单检查ID,无需CAS。适合几乎没有竞争的场景。

  3. 轻量级锁 (Lightweight Locking): 当有轻微竞争时(两个线程交替执行)。线程通过 CAS 操作尝试将对象头替换为指向线程栈中锁记录的指针。成功则获取锁。失败则说明有竞争,升级为重量级锁。自旋消耗CPU。

  4. 重量级锁 (Heavyweight Locking): 当竞争激烈时(多线程同时竞争)。未抢到锁的线程会被阻塞,进入操作系统内核的等待队列,等待操作系统调度唤醒。涉及用户态到内核态切换,开销最大。

升级路径: 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁 (不可逆)

升级目的: 在无竞争或低竞争时,避免昂贵的系统调用(用户态/内核态切换),提高性能。

5. 死锁:四个必要条件与破解之道

死锁就像交通堵塞,多个线程互相等待对方释放资源,导致大家都卡住。发生死锁需要同时满足四个条件:

  1. 互斥 (Mutual Exclusion): 资源一次只能被一个线程使用。(比如锁)

  2. 持有并等待 (Hold and Wait): 线程A拿着资源1,同时还想申请资源2(被线程B拿着),它在等资源2时不会释放资源1。

  3. 不可剥夺 (No Preemption): 资源只能由持有它的线程主动释放,不能被其他线程强行抢走。

  4. 循环等待 (Circular Wait): 线程A等线程B的资源,线程B等线程A的资源,形成一个等待环。

破解死锁: 破坏其中任意一个条件即可。最常用、最实用的方法是破坏循环等待条件

  • 资源顺序申请法: 给所有需要加锁的资源定义一个全局顺序(如按ID大小)。所有线程都严格按照这个顺序去申请资源。

    // 定义资源顺序 (假设 resource1.id 

    这样就不可能形成 线程A锁了资源1等资源2线程B锁了资源2等资源1 的循环等待了。

其他方法:

  • 超时放弃 (tryLock 超时):申请锁超时则放弃并释放已有锁。

  • 检测与恢复:系统检测死锁发生,强制剥夺某个资源或回滚线程。

四、 线程池:管理线程的高手

1. 为什么用线程池?

想象一下,每次有顾客(任务)来,小卖铺就新雇一个收银员(线程),顾客走了就解雇。频繁招人解雇(创建销毁线程)成本(系统开销)太高了!线程池就是开一家有固定/弹性编制收银员的超市:

  • 降低资源消耗: 重用已存在的线程,避免频繁创建销毁线程。

  • 提高响应速度: 任务来了,通常有现成的线程可用,无需等待线程创建。

  • 提高线程可管理性: 统一管理线程的生命周期、数量、行为(如异常处理)。

  • 防止资源耗尽: 控制最大线程数,防止创建过多线程耗尽内存(OOM)或CPU。

2. 线程池核心参数 (ThreadPoolExecutor)

创建线程池最核心的是 ThreadPoolExecutor 的7个参数:

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    corePoolSize,     // YA33: 核心线程数 (常驻员工数)
    maximumPoolSize,  // YA33: 最大线程数 (含核心,最多能招多少人)
    keepAliveTime,    // YA33: 非核心线程空闲多久被回收 (临时工空闲时间)
    unit,             // YA33: keepAliveTime的时间单位 (秒/毫秒等)
    workQueue,        // YA33: 任务队列 (排队区容量和类型)
    threadFactory,    // YA33: 线程工厂 (如何创建线程,可设名字等)
    handler           // YA33: 拒绝策略 (人满+队满时怎么办)
);

工作流程 (YA33小卖铺比喻):

  1. 顾客(任务)来了。

  2. 如果核心收银员(核心线程)有空闲,直接服务(执行任务)。

  3. 如果核心收银员都忙,顾客去排队区(工作队列)等着。

  4. 如果排队区也满了,老板就招临时收银员(非核心线程)来服务新到的顾客(注意:是执行新提交的任务,不是队列里的任务!)。

  5. 如果老板招人也招满了(达到最大线程数),并且还有新顾客来,老板就启动 拒绝策略(handler) 处理这个顾客(比如赶走、自己接待、记下来晚点联系等)。

  6. 当高峰期过去,临时收银员(非核心线程)空闲时间超过 keepAliveTime,就会被解雇(回收)。核心收银员(核心线程)只要线程池不关闭,会一直保留(除非设置 allowCoreThreadTimeOut)。

3. 任务队列 (workQueue) 类型

  • SynchronousQueue (直接传递队列): 容量为0。来一个任务,必须立刻有线程执行它,否则就会尝试创建新线程(如果没到最大线程数)或执行拒绝策略。吞吐量高,避免任务排队。Executors.newCachedThreadPool() 使用它。

  • LinkedBlockingQueue (无界队列): 基于链表,理论容量无限(Integer.MAX_VALUE)。任务来多了都堆积在队列里。最大线程数 maximumPoolSize 参数基本失效。可能导致 OOM。Executors.newFixedThreadPool(), Executors.newSingleThreadExecutor() 使用它。

  • ArrayBlockingQueue (有界队列): 基于数组,固定容量。队列满了才会触发创建非核心线程。更安全可控。生产推荐使用有界队列!

4. 拒绝策略 (handler)

当线程池 关闭 或者 工作队列满且线程数已达 maximumPoolSize 时,新提交的任务会被拒绝。有四种内置策略:

  1. ThreadPoolExecutor.AbortPolicy (默认): 直接抛出 RejectedExecutionException 异常。最严格。

  2. ThreadPoolExecutor.CallerRunsPolicy “调用者运行”策略。被拒绝的任务会退回给提交它的线程(executesubmit 的调用者线程)去执行。让提交任务的线程自己干,可以减缓提交速度。

  3. ThreadPoolExecutor.DiscardPolicy 默默丢弃被拒绝的任务,不抛异常也不做任何处理。佛系。

  4. ThreadPoolExecutor.DiscardOldestPolicy 丢弃工作队列中等待时间最久(队列头)的任务,然后尝试重新提交当前任务。喜新厌旧。

YA33建议: 根据业务场景选择。需要保证不丢任务可以考虑 CallerRunsPolicy 或自定义策略(如记录日志、持久化、转存到其他队列等)。

5. 关闭线程池

  • shutdown() 温和关闭。不再接受新任务,但会执行完已提交的任务(包括队列里的任务)。

  • shutdownNow() 粗暴关闭。尝试停止所有正在执行的任务(通过 Thread.interrupt()),不再处理队列中等待的任务,并返回未执行的任务列表。注意: interrupt() 不一定能立刻停止任务(比如任务没处理中断信号)。

最佳实践: 通常先用 shutdown(),如果希望更快速关闭,可以结合 awaitTermination 等待一段时间,如果超时再用 shutdownNow()

executor.shutdown(); // YA33: 启动有序关闭
try {
    if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { // 等待60秒
        executor.shutdownNow(); // YA33: 超时后强制关闭
    }
} catch (InterruptedException e) {
    executor.shutdownNow();
}

五、 高级工具点睛 (JUC 利器)

1. CountDownLatch (倒计时门闩)

作用: 让一个或多个线程等待其他一组线程完成各自工作后再继续执行。就像运动会赛跑,所有运动员(工作线程)都到达终点(countDown())后,裁判(await() 的线程)才能宣布比赛结束。

public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        int workerCount = 3;
        CountDownLatch latch = new CountDownLatch(workerCount); // YA33: 倒计时3次

        for (int i = 0; i  {
                System.out.println("YA33-工人" + Thread.currentThread().getId() + "干活...");
                try {
                    Thread.sleep((long) (Math.random() * 2000)); // 模拟工作时间
                } catch (InterruptedException e) {}
                System.out.println("YA33-工人" + Thread.currentThread().getId() + "干完了!");
                latch.countDown(); // YA33: 完成工作,倒计时-1
            }).start();
        }

        System.out.println("YA33-老板等所有工人完工...");
        latch.await(); // YA33: 老板(主线程)在此等待,直到倒计时为0
        System.out.println("YA33-老板:所有工人完工,收工!");
    }
}

2. Semaphore (信号量)

作用: 控制同时访问特定资源的线程数量。就像一个只有N个座位的候诊室(permits=N)。线程(acquire())拿到一个座位(许可)才能进去看病(访问资源),看完病出来(release())释放座位给下一个人。

public class SemaphoreDemo {
    public static void main(String[] args) {
        int permits = 2; // 只有2个许可证 (资源)
        Semaphore semaphore = new Semaphore(permits);

        for (int i = 1; i  {
                try {
                    String threadName = Thread.currentThread().getName();
                    semaphore.acquire(); // YA33: 获取许可 (没许可就阻塞)
                    System.out.println("YA33-" + threadName + "抢到资源,开始使用...");
                    Thread.sleep(2000); // 模拟使用资源时间
                    System.out.println("YA33-" + threadName + "使用完毕,释放资源");
                } catch (InterruptedException e) {
                } finally {
                    semaphore.release(); // YA33: 释放许可 (必须在finally!)
                }
            }, "线程" + i).start();
        }
    }
}

3. Future & Callable (异步结果)

  • Callable 类似 Runnable,但它能返回结果,也能抛出异常

  • Future 表示异步计算的结果。提供了检查计算是否完成(isDone())、获取结果(get(), 会阻塞)、取消任务(cancel())的方法。

ExecutorService executor = Executors.newFixedThreadPool(2);

// YA33: 提交Callable任务,返回Future
Future future = executor.submit(() -> {
    System.out.println("YA33-Callable任务计算中...");
    Thread.sleep(1000);
    return 42; // 返回结果
});

// ... 主线程可以继续做其他事情 ...

try {
    Integer result = future.get(); // YA33: 阻塞等待结果
    System.out.println("YA33-计算结果: " + result);
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}

executor.shutdown();

4. ConcurrentHashMap (并发哈希表)

作用: 线程安全的 HashMap。相比 Collections.synchronizedMap() 包装的 HashMap 性能更好,特别是在读多写少的场景。它采用分段锁(JDK7)或 CAS+synchronized(JDK8+)等技术实现高并发。

ConcurrentHashMap map = new ConcurrentHashMap();
map.put("YA33-Score", 100);
// 并发安全的操作
map.computeIfPresent("YA33-Score", (k, v) -> v + 10);

结语

你已经完成了Java多线程的入门之旅。从基础概念到线程创建、状态管理、通信协作,再到线程安全、锁机制、线程池和JUC工具,我们一路打怪升级过来。多线程编程是Java中一个既重要又有挑战性的领域,关键是理解原理多实践。记住几个核心:

  1. 安全第一: 时刻警惕共享数据的线程安全问题,善用锁、原子类等机制。

  2. 性能优先: 优先考虑线程池、非阻塞算法(CAS)、减少锁竞争。

  3. 选对工具: 理解 synchronized, Lock, volatile, 原子类, 阻塞队列, CountDownLatch, Semaphore 等工具的特性和适用场景。

  4. 避免雷区: 小心死锁、资源耗尽(OOM)、过度同步、volatile 误用等问题。

文章来源于互联网:java基础(五)多线程篇

相关推荐: 教你用Lovart+Midjourney轻松批量输出分镜图!

嗨大家好!我是阿真! 响应群友催更,今天我们来看点轻轻松松的,Lovart第三弹,分享关于Midjourney在Lovart工作流与其他模型批量输出分镜图的一些方法给大家。 上链接:https://www.lovart.ai/zh Lovart最近把Midjo…

赞(0)
未经允许不得转载:5bei.cn大模型教程网 » java基础(五)多线程篇
分享到: 更多 (0)

AI大模型,我们的未来

小欢软考联系我们