【Java基础整理】Java多线程

0 阅读13分钟

Java多线程详解

1. 多线程概述

什么是线程

线程是程序中的执行线程。Java虚拟机允许应用程序并发地运行多个执行线程。

核心要点:

  • 线程是程序执行的最小单位
  • 多线程可以提高程序的执行效率
  • 线程之间可以并发执行,共享进程资源
  • 正确创建和使用多个线程至关重要

2. 创建线程的两种方法

Java中创建线程有两种主要方法:继承Thread类和实现Runnable接口。

2.1 方法一:继承Thread类

实现步骤:

  1. 继承Thread类
  2. 重写Thread类的run()方法
  3. 调用线程的start()方法启动线程

特点: start()方法开启run()并发执行该线程。

完整示例
// 第一个线程类继承Thread类
public class Thread1 extends Thread {
    public void run() {
        // 执行60次循环输出
        for (int i = 0; i < 60; i++) {
            System.out.println("ThreadOne::" + i);
        }
    }
}

// 第二个线程类继承Thread类
public class Thread2 extends Thread {
    public void run() {
        // 执行60次循环输出
        for (int i = 0; i < 60; i++) {
            System.out.println("ThreadTwo::: " + i);
        }
    }
}

// 主方法
public class ThreadMain {
    // 同步执行两个线程
    public static void main(String[] args) {
        // 创建两个线程对象
        Thread1 t1 = new Thread1();
        Thread2 t2 = new Thread2();
        
        // 使用线程的start方法同步执行两个线程对象
        t1.start();
        t2.start();
    }
}

输出结果: 两个线程对象随机交替执行输出语句。

2.2 方法二:实现Runnable接口(推荐)

实现步骤:

  1. 定义类实现Runnable接口
  2. 覆盖Runnable接口中的run()方法,将线程需要运行的代码写入
  3. 通过Thread类建立带Runnable接口的子类对象参数的线程对象
  4. 调用Thread类的start()方法开启线程并调用Runnable接口子类的run()方法
完整示例:模拟卖票窗口
// 实现Runnable接口的线程类,模拟卖票窗口
public class Ticket implements Runnable {
    // 定义票数
    private int tick = 100;
    
    // 重写Runnable接口run()方法
    public void run() {
        while (true) {
            if (tick > 0) {
                // 这里多个线程内共享一个tick对象
                System.out.println(Thread.currentThread().getName() + 
                                 ":sale:" + tick--);
            } else {
                // 卖完打烊
                break;
            }
        }
    }
}

public class TicketMain {
    public static void main(String[] args) {
        // 建立实现Runnable接口的线程对象
        Ticket t = new Ticket();
        
        // 将实现Runnable接口的子类当作Thread类的参数,
        // 用Thread类的start()方法来开启多个线程
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        t1.start();
        t2.start();
    }
}

2.3 两种方法的对比

比较项目继承Thread类实现Runnable接口
继承限制无法再继承其他类避免单继承局限性 ✓
资源共享每个线程独立的资源多个线程共享同一个对象 ✓
获取当前线程直接使用this使用Thread.currentThread()
代码复用性较差较好 ✓
推荐程度不推荐推荐使用

结论: 实现Runnable接口的方式更加灵活,是创建线程的推荐方法。

3. 线程安全性

3.1 线程安全问题

问题描述: 当多个线程同时操作同一个共享数据时,如果一个线程对多条语句只执行了一部分,还没全部执行完,另一个线程就参与进来执行,可能会导致共享数据的错误。

解决方案: 对多条操作共享数据的语句,只能让一个线程执行完,才可以让其他线程参与运行。

3.2 线程同步机制

Java提供了synchronized关键字实现线程同步(JDK 1.5之前的主要方式)。

同步代码块
// 定义锁对象
Object obj = new Object();

synchronized(obj) {
    // 需要同步的代码块
}
同步方法

概念: 同步方法就是给某个方法加锁,从而避免不同对象调用同一个方法出现数据错误的问题。

完整银行存款示例:

// 描述银行
public class Bank {
    private int sum;
    
    // 银行具有存钱方法,因为执行语句较多所以需要方法的同步
    // 方法的同步锁是this
    public synchronized void add(int n) {
        sum = sum + n;
        try {
            Thread.sleep(10);
        } catch(InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("sum=" + sum);
    }
}

// 描述操作银行的线程类,run方法是操作银行存入动作
public class Cus implements Runnable {
    private Bank b = new Bank();
    
    // 存三次,每次存入100
    public void run() {
        for (int x = 0; x < 3; x++) {
            b.add(100);
        }
    }
}

// 主方法
public class BankMain {
    public static void main(String[] args) {
        // 创建线程,实现多线程操作
        Cus c = new Cus();
        Thread t1 = new Thread(c);
        Thread t2 = new Thread(c);
        t1.start();
        t2.start();
    }
}

3.3 同步锁的说明

同步类型锁对象说明
同步代码块自定义对象可以是任意对象
同步方法this当前实例对象
静态同步方法类名.class类对象

⚠️ 重要提示: 多线程死锁问题需要特别注意,避免相互等待造成程序停止。

4. 线程的生命周期

4.1 线程状态详解

新建 → 就绪 ⇄ 运行 → 消亡
        ↕     ↓
      阻塞 ←───
1. 新建状态(New)
  • 触发条件: 当new一个Thread或其子类建立线程后
  • 状态描述: 线程对象处于新生状态,还未启动
2. 就绪状态(Runnable)
  • 触发条件: 调用start()方法后进入
  • 状态描述: 线程具备了运行条件,但未必立刻执行
  • 💡 提示: 可使用Thread.sleep()方法使主线程休眠,让其他线程有机会执行
3. 运行状态(Running)
  • 触发条件: 线程抢到CPU执行权
  • 可能的操作:
    • 睡眠操作: 调用sleep()方法 → 进入阻塞状态
    • 等待操作: 调用wait()方法 → 进入阻塞状态(需要notify()notifyAll()唤醒)
    • IO阻塞: 调用阻塞式IO方法 → 进入阻塞状态
    • 退让暂停: 调用yield()方法 → 重新进入就绪状态(不进入阻塞状态)
4. 阻塞状态(Blocked)
  • 进入条件: 由运行状态转变而来
  • 阻塞类型及恢复方式:
阻塞类型进入方式恢复方式
睡眠阻塞sleep()指定时间后自动恢复到就绪状态
等待阻塞wait()notify()notifyAll()唤醒
IO阻塞阻塞式IO方法外设完成IO操作后自动恢复
5. 消亡状态(Dead)
  • 触发条件:run()方法正常退出
  • 状态描述: 线程执行结束,进入消亡状态

4.2 状态转换图

[新建] --start()--> [就绪] <--yield()-- [运行]
                     ↑                    ↓
                     |              sleep()/wait()/IO阻塞
                     |                    ↓
                   恢复 <------------- [阻塞]
                     ↑                    
                     |                    
[消亡] <--run()结束-- [运行]

5. 等待唤醒操作(线程间通信)

5.1 等待唤醒机制概述

使用场景: 多个线程同步操作,但操作的动作不同。比如:对同一个资源的存储和读取操作。

目标: 让CPU运行一次存储线程,就运行一次读取线程。

核心方法: wait()notify()notifyAll()

5.2 等待唤醒方法说明

方法名功能描述使用方式
wait()让当前线程进入等待状态this.wait()
notify()唤醒等待池中的一个线程this.notify()
notifyAll()唤醒等待池中的所有线程this.notifyAll()

⚠️ 注意事项:

  1. 等待和唤醒方法必须标识出所属的锁
  2. 必须在同步中使用,因为要对持有监视器(锁)的线程操作
  3. 操作的线程必须使用同一个锁

5.3 生产者-消费者模式实现

基础版本(适用于单生产者-单消费者)
// 封装商品和生产、消费商品的方法
public class Resource {
    private String name;
    private int count = 1;  // 产品编号
    private boolean flag = false;  // 标记:true可消费,false可生产
    
    // 生产操作,如果消费过一次就得存入一次商品
    public synchronized void set(String name) {
        if (flag) {
            try { 
                wait(); 
            } catch(InterruptedException e) {}
        }
        
        this.name = name + "..." + count++;
        System.out.println(Thread.currentThread().getName() + 
                          "..生产。。" + this.name);
        
        // 改变标记,好让消费线程成功运行,true代表可消费
        flag = true;
        // 仅考虑两个线程操作,所以唤醒对方
        this.notify();
    }
    
    // 消费操作,如果生产过商品了,就消费一次商品
    public synchronized void out() {
        if (!flag) {
            try { 
                wait(); 
            } catch(InterruptedException e) {}
        }
        
        System.out.println(Thread.currentThread().getName() + 
                          "。消费。" + this.name);
        
        // 改变标记,生产线程false代表可生产
        flag = false;
        // 仅考虑两个线程操作,所以唤醒对方
        this.notify();
    }
}

// 生产者线程
public class Producer implements Runnable {
    private Resource res;
    
    Producer(Resource res) {
        this.res = res;
    }
    
    public void run() {
        while (true) {
            res.set(".商品.");
        }
    }
}

// 消费者线程
public class Consumer implements Runnable {
    private Resource res;
    
    Consumer(Resource res) {
        this.res = res;
    }
    
    public void run() {
        while (true) {
            res.out();
        }
    }
}

// 主方法
public class ProducerConsumerMain {
    public static void main(String[] args) {
        // 创建商品库,用来存商品和消费商品
        Resource r = new Resource();
        
        // 启动生产、消费两个线程
        new Thread(new Producer(r)).start();
        new Thread(new Consumer(r)).start();
    }
}
多生产者-多消费者问题

问题: 上面的示例只适合一个生产线程和一个消费线程。如果创建多个生产线程和多个消费线程,系统将无法保证生产一次便消费一次。

解决方案:

  1. 将标记判断if改为while让其循环判断
  2. notify()换为notifyAll(),唤醒所有等待的线程

问题: 使用notifyAll()会唤醒所有等待线程,消耗更多系统资源。

更好的解决方案: JDK 1.5后的Lock、Condition接口

5.4 Lock、Condition接口(JDK 1.5+)

改进要点:

  • synchronizedLock显示锁操作
  • wait/notify/notifyAllConditionawait/signal/signalAll
  • Lock支持创建多个相关的Condition对象
  • 可以精确控制哪些线程被唤醒
核心接口说明
传统方式Lock方式说明
synchronizedLock.lock() / Lock.unlock()显式锁操作
wait()Condition.await()线程等待
notify()Condition.signal()唤醒一个线程
notifyAll()Condition.signalAll()唤醒所有线程
Lock版本的生产者-消费者实现
import java.util.concurrent.locks.*;

// 使用Lock、Condition接口的Resource类
public class Resource {
    private String name;
    private int count = 1;
    private boolean flag = false;
    
    // 应用Lock、Condition接口
    private Lock lock = new ReentrantLock();
    // 创建两个相关Condition对象
    private Condition condition_pro = lock.newCondition();  // 生产者等待条件
    private Condition condition_con = lock.newCondition();  // 消费者等待条件
    
    // 生产操作
    public void set(String name) {
        lock.lock();
        try {
            while (flag) {
                // 生产线程等待
                condition_pro.await();
            }
            
            this.name = name + "..." + count++;
            System.out.println(Thread.currentThread().getName() + 
                              "..生产。。" + this.name);
            
            // 改变标记,好让消费线程成功运行,true代表可消费
            flag = true;
            // 精确唤醒消费线程
            condition_con.signal();
            
        } catch(InterruptedException e) {
        } finally {
            lock.unlock();
        }
    }
    
    // 消费操作
    public void out() {
        lock.lock();
        try {
            while (!flag) {
                // 消费线程等待
                condition_con.await();
            }
            
            System.out.println(Thread.currentThread().getName() + 
                              "。消费。" + this.name);
            
            // 改变标记,生产线程false代表可生产
            flag = false;
            // 精确唤醒生产线程
            condition_pro.signal();
            
        } catch(InterruptedException e) {
        } finally {
            lock.unlock();
        }
    }
}

优势:

  1. 精确唤醒:可以指定唤醒特定类型的线程
  2. 资源节省:避免唤醒不必要的线程
  3. 程序灵活性:支持更复杂的线程协调场景

6. 线程的其他重要方法

6.1 线程控制方法

方法名功能描述使用说明
stop()停止线程已过时,现在通过控制run()方法结束来停止线程
interrupt()中断线程清除冻结状态,强制让线程恢复到运行状态
setDaemon(boolean on)设置守护线程设为true时,主线程结束,守护线程也自动结束
join()等待线程结束当前线程放弃执行权,等待调用join()的线程执行完
方法详细说明

1. interrupt() 方法

// 中断线程示例
public class InterruptDemo {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    Thread.sleep(1000);
                    System.out.println("线程运行中...");
                } catch (InterruptedException e) {
                    System.out.println("线程被中断");
                    Thread.currentThread().interrupt(); // 重新设置中断标志
                    break;
                }
            }
        });
        
        t.start();
        
        // 3秒后中断线程
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.interrupt();
    }
}

2. join() 方法

// join方法示例
public class JoinDemo {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("t1: " + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        
        t1.start();
        
        try {
            t1.join(); // 主线程等待t1执行完成
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println("主线程继续执行");
    }
}

6.2 线程信息方法

方法名功能描述返回值
toString()获取线程信息线程名、优先级、线程组
setPriority(int newPriority)设置线程优先级void
getPriority()获取线程优先级int
yield()线程让步void
线程优先级

优先级常量:

  • Thread.MIN_PRIORITY = 1(最低优先级)
  • Thread.NORM_PRIORITY = 5(普通优先级,默认)
  • Thread.MAX_PRIORITY = 10(最高优先级)
// 线程优先级示例
public class PriorityDemo {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println("低优先级线程: " + i);
            }
        });
        
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println("高优先级线程: " + i);
            }
        });
        
        t1.setPriority(Thread.MIN_PRIORITY);
        t2.setPriority(Thread.MAX_PRIORITY);
        
        t1.start();
        t2.start();
    }
}

3. yield() 方法

// yield方法示例
public class YieldDemo {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println("线程1: " + i);
                if (i == 5) {
                    Thread.yield(); // 让步给其他线程
                }
            }
        });
        
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println("线程2: " + i);
            }
        });
        
        t1.start();
        t2.start();
    }
}

7. 线程池(高级主题)

7.1 线程池概述

线程池的优势:

  1. 降低资源消耗:重复利用已创建的线程
  2. 提高响应速度:任务到达时不需要等待线程创建
  3. 提高线程的可管理性:统一分配、调优和监控

7.2 线程池的使用

import java.util.concurrent.*;

public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 创建固定大小的线程池
        ExecutorService executor = Executors.newFixedThreadPool(3);
        
        // 提交任务
        for (int i = 0; i < 10; i++) {
            final int taskId = i;
            executor.submit(() -> {
                System.out.println("任务 " + taskId + " 在线程 " + 
                                 Thread.currentThread().getName() + " 中执行");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        
        // 关闭线程池
        executor.shutdown();
    }
}

7.3 常用线程池类型

线程池类型创建方式特点
固定大小线程池newFixedThreadPool(n)线程数固定为n
缓存线程池newCachedThreadPool()线程数根据需要动态创建
单线程线程池newSingleThreadExecutor()只有一个工作线程
调度线程池newScheduledThreadPool(n)支持定时和周期性任务

8. 常见异常处理

8.1 多线程相关异常

异常名称异常描述常见原因处理建议
IllegalThreadStateException线程状态不当异常线程没有处于操作所需的适当状态检查线程状态再进行操作
InterruptedException线程中断异常线程在等待、休眠或暂停状态被其他线程中断捕获异常并适当处理中断
SecurityException安全异常当前线程无权修改目标线程检查安全管理器设置

8.2 异常处理示例

public class ThreadExceptionDemo {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            try {
                while (!Thread.currentThread().isInterrupted()) {
                    // 模拟工作
                    Thread.sleep(1000);
                    System.out.println("工作中...");
                }
            } catch (InterruptedException e) {
                System.out.println("线程被中断: " + e.getMessage());
                // 处理中断
                Thread.currentThread().interrupt();
            }
        });
        
        try {
            t.start();
            Thread.sleep(3000);
            t.interrupt(); // 中断线程
        } catch (InterruptedException e) {
            System.out.println("主线程被中断: " + e.getMessage());
        }
    }
}

9. 多线程最佳实践

9.1 线程安全编程原则

  1. 最小化共享数据:尽量减少线程间共享的数据
  2. 使用不可变对象:不可变对象天然线程安全
  3. 正确使用同步:避免过度同步和同步不足
  4. 避免死锁:注意锁的获取顺序

9.2 性能优化建议

  1. 选择合适的创建方式:优先使用实现Runnable接口
  2. 使用线程池:避免频繁创建和销毁线程
  3. 合理设置线程优先级:避免优先级倒置问题
  4. 及时释放资源:确保锁能够正确释放

9.3 调试多线程程序

public class ThreadDebugDemo {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println("线程名称: " + Thread.currentThread().getName());
            System.out.println("线程ID: " + Thread.currentThread().getId());
            System.out.println("线程优先级: " + Thread.currentThread().getPriority());
            System.out.println("是否为守护线程: " + Thread.currentThread().isDaemon());
        }, "调试线程");
        
        t.start();
        
        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println("主线程结束");
    }
}

10. 总结

多线程的核心概念

  1. 线程创建:实现Runnable接口是推荐方式
  2. 线程安全:使用同步机制保护共享资源
  3. 线程通信:wait/notify或Lock/Condition实现线程协作
  4. 线程控制:合理使用各种线程控制方法

开发建议

场景推荐方式理由
简单并行任务实现Runnable接口避免继承限制
复杂线程协作Lock + Condition更精确的控制
大量短期任务线程池提高性能和可管理性
定时任务ScheduledExecutorService专业的调度功能

注意事项

  1. 避免滥用synchronized:过度同步会降低性能
  2. 正确处理InterruptedException:不要忽略线程中断
  3. 谨慎使用stop()方法:已过时,可能导致数据不一致
  4. 合理设计线程间通信:使用合适的同步机制

多线程编程是Java开发中的重要技能,正确理解和使用多线程可以显著提高程序的性能和用户体验。但同时也要注意线程安全问题,避免出现竞态条件和死锁等问题。


本文全面介绍了Java多线程编程的核心概念、实现方式、同步机制和最佳实践,希望对深入理解和掌握Java多线程技术有所帮助。