什么是多线程?

在 Java 中,多线程是一种特殊的编程模式,它允许一个程序同时执行多个任务。你可以把它想象成一个厨房,一个厨师(单线程)一次只能切菜、炒菜、洗碗,所有事情都得按顺序来。而多线程就像是这个厨房里同时有多个厨师(多线程),一个在切菜,一个在炒菜,一个在洗碗,大家各司其职,大大提高了工作效率。

在计算机中,进程是程序的一次执行过程,它拥有独立的内存空间。而线程是进程中的一个执行单元,一个进程可以包含多个线程,这些线程共享进程的内存空间。

为什么要用多线程?

多线程在很多场景下都非常有用:

  • 提高程序效率: 比如在下载一个大文件时,可以同时开启多个线程来下载文件的不同部分,最后再合并起来,这样比单线程下载要快得多。
  • 改善用户体验: 在图形用户界面(GUI)中,如果主线程正在处理一个耗时的任务(比如处理图片),界面就会卡住,用户无法进行其他操作。通过把这个任务放到一个独立的线程中,主线程就可以继续响应用户的点击等操作,保证界面的流畅性。
  • 利用多核处理器: 现代计算机大多是多核处理器,多线程可以充分利用这些硬件资源,让多个线程在不同的CPU核心上并行执行,从而真正实现“同时”执行,显著提高计算密集型任务的性能。

如何创建和启动线程?

在 Java 中,创建和启动线程主要有两种方式:

  1. 继承 Thread

    创建一个类,让它继承 java.lang.Thread,然后重写 run() 方法。run() 方法包含了线程要执行的任务代码。

    class MyThread extends Thread {
        public void run() {
            System.out.println("这是一个继承 Thread 类创建的线程!");
        }
    }
    
    // 启动线程
    MyThread myThread = new MyThread();
    myThread.start(); // 调用 start() 方法来启动线程,而不是直接调用 run()
    
  2. 实现 Runnable 接口

    创建一个类,让它实现 java.lang.Runnable 接口,并实现 run() 方法。然后将这个类的实例作为参数传入 Thread 类的构造函数来创建线程。

    class MyRunnable implements Runnable {
        public void run() {
            System.out.println("这是一个实现 Runnable 接口创建的线程!");
        }
    }
    
    // 启动线程
    MyRunnable myRunnable = new MyRunnable();
    Thread thread = new Thread(myRunnable);
    thread.start();
    

推荐使用 Runnable 接口,因为它更灵活:

  • 避免单继承的局限性: Java 是单继承的,如果你的类已经继承了其他类,就无法再继承 Thread 了。
  • 更好的资源共享: 多个线程可以共享同一个 Runnable 对象,这在处理共享数据时非常方便。

线程的生命周期

线程从创建到死亡会经历以下几个阶段:

  1. 新建(New): 当你使用 new 关键字创建一个 Thread 对象时,线程就进入了新建状态。
  2. 可运行(Runnable): 当你调用 start() 方法后,线程进入可运行状态。此时它等待操作系统的调度,获得 CPU 时间片后就会开始执行。
  3. 运行(Running): 线程获得 CPU 时间片,正在执行 run() 方法中的代码。
  4. 阻塞(Blocked): 线程因为某种原因暂停执行,比如等待 I/O 操作、等待获取锁或调用了 sleep() 方法。当条件满足时,它会重新回到可运行状态。
  5. 终止(Terminated): 线程的 run() 方法执行完毕或者因为异常退出,线程进入终止状态。

线程同步

多线程虽然好,但也带来了新的问题:线程安全。当多个线程同时访问和修改同一个共享资源时,可能会导致数据不一致。

想象一下,一个银行账户有1000元,线程 A 取款 500元,线程 B 也取款 500元。如果没有同步机制,可能会出现以下情况:

  1. 线程 A 读取余额(1000元)。
  2. 线程 B 读取余额(1000元)。
  3. 线程 A 计算新余额(1000 - 500 = 500)。
  4. 线程 B 计算新余额(1000 - 500 = 500)。
  5. 线程 A 写入新余额(500元)。
  6. 线程 B 写入新余额(500元)。

最终,账户余额变成了500元,而不是0元,这就是数据不一致。

为了解决这个问题,Java 提供了多种同步机制:

  1. synchronized 关键字:

    synchronized 可以修饰方法或代码块。它能确保在同一时刻,只有一个线程可以访问被它修饰的代码。

    • 修饰方法: 锁住的是当前对象实例。
    • 修饰静态方法: 锁住的是当前类的 Class 对象。
    • 修饰代码块: 锁住的是括号中的对象。这是一种更灵活的方式,可以只同步需要同步的代码,提高效率。
    public class BankAccount {
        private int balance = 1000;
    
        // 使用 synchronized 关键字同步方法
        public synchronized void withdraw(int amount) {
            if (balance >= amount) {
                try {
                    Thread.sleep(100); // 模拟耗时操作
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                balance -= amount;
                System.out.println("取款成功,账户余额:" + balance);
            } else {
                System.out.println("余额不足!");
            }
        }
    }
    
  2. Lock 接口(java.util.concurrent.locks.Lock):

    Lock 接口提供了比 synchronized 更强大的功能和更高的灵活性。最常用的实现类是 ReentrantLock

    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class BankAccountWithLock {
        private int balance = 1000;
        private final Lock lock = new ReentrantLock();
    
        public void withdraw(int amount) {
            lock.lock(); // 获取锁
            try {
                if (balance >= amount) {
                    balance -= amount;
                    System.out.println("取款成功,账户余额:" + balance);
                } else {
                    System.out.println("余额不足!");
                }
            } finally {
                lock.unlock(); // 释放锁
            }
        }
    }
    

    使用 Lock 时,一定要在 finally 块中调用 unlock() 方法来释放锁,以避免死锁。

线程池

频繁地创建和销毁线程会消耗大量的系统资源。线程池(Thread Pool)是一种管理和复用线程的机制,可以解决这个问题。

你可以把它想象成一个员工池。当有任务来时,不是临时招聘一个员工,而是从员工池里找一个空闲的员工去处理;任务完成后,员工不是被解雇,而是回到员工池等待下一个任务。

Java 的 Executor 框架提供了线程池的实现,主要通过 Executors 工厂类来创建不同类型的线程池:

  • newFixedThreadPool(int nThreads):创建一个固定大小的线程池,当提交的任务数多于线程池大小时,任务会在队列中等待。
  • newCachedThreadPool():创建一个可缓存的线程池,如果线程池大小超过处理任务所需,空闲的线程会被回收。如果任务增加,则会创建新线程。
  • newSingleThreadExecutor():创建一个单线程的线程池,它只会用唯一的工作线程来执行任务,保证所有任务都是按顺序执行的。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolExample {
    public static void main(String[] args) {
        // 创建一个包含两个线程的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        // 提交任务给线程池
        for (int i = 0; i < 5; i++) {
            final int taskIndex = i;
            executorService.execute(() -> {
                System.out.println("线程 " + Thread.currentThread().getName() + " 正在执行任务 " + taskIndex);
            });
        }
        
        // 关闭线程池
        executorService.shutdown();
    }
}

总结

多线程是 Java 并发编程的核心,它能够有效提升程序的性能和响应速度。但与此同时,也带来了线程安全、死锁等挑战。掌握线程的创建、生命周期、同步机制以及线程池的使用,是写出高效、健壮的 Java 应用的关键。

更多推荐