Java 多线程入门教程

线程与进程的基本概念

线程 (Thread) 是进程内的一个执行单元,每个线程有自己的执行路径。线程比进程更轻量,一个进程中可以包含多个线程,它们共享进程的堆栈空间和资源(如内存、文件等),但每个线程有自己的寄存器上下文和调用堆栈。

进程 (Process) 是操作系统分配资源的基本单位,每个进程有独立的内存空间和资源。简而言之,进程是线程的容器;线程更轻量,创建和销毁开销小,并且线程之间可以共享数据,而不同进程之间相互独立。

多线程的优势包括:提高系统吞吐率(通过并发执行多个任务,提高整体处理能力);提高程序响应性(如 Web 服务器用专门线程处理请求,缩短用户等待时间);充分利用多核CPU资源,实现并行计算。
多线程的风险主要体现在并发问题上:如果多个线程同时访问共享数据而缺乏适当的同步控制,就可能产生数据不一致、读取到过期数据或丢失更新等线程安全问题。并发还可能引发死锁等活性问题(例如两个线程互相等待对方持有的锁而都无法继续)。此外,线程切换也有一定开销,过多线程可能带来性能下降。因此,多线程设计时需要权衡并发带来的性能提升和复杂性风险。

Java 中创建线程的三种方式

在 Java 中常见的创建线程方式有三种:继承 Thread 类、实现 Runnable 接口,以及使用线程池(ExecutorService)。

  • 方式1:继承 Thread 类

    继承 java.lang.Thread 类并重写其 run() 方法,然后创建子类实例并调用 start() 启动线程。例如:

    // 定义线程类,继承 Thread
    class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("线程开始执行:" + Thread.currentThread().getName());
        }
    }
    
    public class ThreadDemo {
        public static void main(String[] args) {
            MyThread t1 = new MyThread();
            MyThread t2 = new MyThread();
            t1.start(); // 启动线程 t1 
            t2.start(); // 启动线程 t2 
        }
    }

    这里,MyThread 类继承了 Threadrun() 方法定义了线程执行的任务。调用 start() 时,JVM 会创建新线程并执行 run() 方法中的逻辑。

  • 方式2:实现 Runnable 接口

    实现 Runnable 接口重写 run() 方法,然后将其作为参数传给 Thread 构造器,最后调用 Threadstart() 方法。例如:

    // 定义实现 Runnable 的任务类
    class MyRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println("线程开始执行:" + Thread.currentThread().getName());
        }
    }
    
    public class RunnableDemo {
        public static void main(String[] args) {
            Thread t1 = new Thread(new MyRunnable());
            Thread t2 = new Thread(new MyRunnable());
            t1.start(); // 启动线程 t1
            t2.start(); // 启动线程 t2
        }
    }
     

    这种方式适用于多个线程共享同一个任务对象,或类已继承自其它类的场景。相比继承 Thread,实现 Runnable 更灵活,且任务类与线程类职责分离。

  • 方式3:使用线程池

    Java 提供 java.util.concurrent.Executors 工具类来创建线程池,管理线程的创建和回收。使用线程池可以复用线程、控制并发线程数,避免频繁创建/销毁线程的开销。例如:

    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    // 定义任务
    class Task implements Runnable {
        @Override
        public void run() {
            System.out.println("任务开始执行:" + Thread.currentThread().getName());
        }
    }
    
    public class ThreadPoolDemo {
        public static void main(String[] args) {
            // 创建一个固定大小为3的线程池
            ExecutorService executor = Executors.newFixedThreadPool(3); // 提交多个任务
            for (int i = 0; i < 5; i++) {
                executor.execute(new Task());
            }
            // 关闭线程池
            executor.shutdown();
        }
    }

    上例中通过 Executors.newFixedThreadPool(3) 获得一个固定大小为 3 的线程池。线程池会复用线程来执行任务,超过 3 个任务时,新任务会排队等待;任务执行完毕后,线程归还给池,由 shutdown() 方法关闭线程池。

线程的生命周期


线程从创建到结束经历多个状态,常见的基本状态包括:新建(New)就绪(Runnable)运行(Running)阻塞(Blocked)死亡(Terminated)。例如,创建线程对象后,它处于新建状态;调用 start() 后进入就绪状态,等待线程调度;一旦线程获得 CPU 时间,就进入运行状态开始执行;执行中如果调用 sleep() 或等待锁等操作,则会进入阻塞状态(无执行资格);阻塞结束后返回就绪/运行。线程执行完 run() 方法或抛出异常后,进入死亡状态,线程终止。上图展示了这些状态及其转换关系,包括线程的创建(start)、运行(run)、休眠(sleep/等待)和终止等流程。

线程同步与 synchronized

多线程环境下,多个线程并发访问共享资源会引发线程安全问题(如竞态条件、数据不一致等)。Java 提供 synchronized 关键字来实现同步,保证同一时间只有一个线程可以执行被锁定的代码块,从而保护共享资源。例如:

class SafeCounter {
    private int count = 0; // 同步实例方法,锁对象为当前实例 

    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

在上述代码中,increment() 方法被声明为 synchronized,等价于同步锁住当前 SafeCounter 对象(即锁对象为 this)。当多个线程调用该方法时,JVM 保证同一时刻只有一个线程持有锁并执行 count++,避免并发修改带来的数据紊乱。与之相对,如果取消 synchronized,两个线程同时执行 count++ 可能造成最终结果小于预期(丢失更新),即发生竞态条件。

除了同步实例方法,synchronized 也可以用于同步代码块并指定锁对象:

public void doWork(Object lock) {
    synchronized (lock) {
        // 只有持有 lock 对象锁的线程才能执行此处代码
        // 对共享资源的访问 
    }
}

括号中的 lock 对象可以是任意非空引用类型,多个线程如果 synchronized 同一对象,就会产生锁竞争。另外,如果将 synchronized 用于静态方法,则锁对象为对应的类对象(Class),而非 this

synchronized vs ReentrantLock

除了 synchronized,Java 还提供了 java.util.concurrent.locks.ReentrantLock 类进行手动加锁。相同点ReentrantLocksynchronized 本质上都能实现线程互斥和内存可见性,并且都是可重入锁。不同点

  • 功能:ReentrantLock 支持可中断锁定限时锁定和公平锁等高级功能,而 synchronized 不支持超时等待和中断等待。例如,当线程 A 已持有锁,线程 B 等待锁时,使用 synchronized B 只能一直阻塞等待无法中断;而使用 ReentrantLock,可调用 lockInterruptibly()tryLock(timeout) 来支持超时或响应中断。

  • 释放方式:synchronized 是 JVM 原语,在抛出异常时会自动释放锁;而 ReentrantLock 是通过代码实现的,必须在 try{...} finally{ lock.unlock(); } 中手动调用 unlock() 来释放锁,否则容易造成死锁。

  • 性能:在低争用情况下,synchronized 已经过度优化(偏向锁、轻量级锁等),性能往往优于 ReentrantLock;但在高并发强争用场景下,ReentrantLock 性能更稳定。

示例如下:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private static int count = 0;
    private static Lock lock = new ReentrantLock();

    public static void increment() {
        lock.lock();
        // 获取锁
        try {
            count++;
        } finally {
            lock.unlock(); // 必须在 finally 中释放锁
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 启动多个线程来调用 increment() 
    }
}

上例通过 lock.lock() 获取锁,执行关键操作后在 finally 中调用 lock.unlock() 释放锁。这种显式加锁方式相比 synchronized 更灵活(可以尝试加锁 tryLock()、设置超时时间、响应中断等),但也要注意释放锁的调用位置。

volatile 关键字

volatile 修饰的变量具有 可见性 保证,即写操作会直接写入主内存,读操作总是从主内存获取,从而确保一个线程对该变量的修改能被其他线程立即看到。volatile 无法保证操作的原子性,只保证内存可见性。常用于轻量级的同步场景,例如用作线程之间的停止标志:
 

public class VolatileExample {
    private static volatile boolean running = true;

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (running) {
                // 线程执行任务 
            }
            System.out.println("线程结束");
        });
        t.start();
        Thread.sleep(1000);
        running = false; // 主线程修改 running,子线程可见后跳出循环 
    }
}

在上例中,running 被声明为 volatile,主线程修改后,子线程能够立即感知到值的变化,从而退出循环。volatile 常用于状态标志、双重检查锁定等需要保证可见性的场景。

线程安全、竞态条件与死锁

线程安全(thread safety)指在多线程环境下程序运行结果仍正确,一段代码在多个线程同时访问时总能按预期工作,而无需额外的同步措施。相对地,线程不安全的代码在并发时会产生错误。线程安全的本质是对共享可变数据的访问要么设计为无竞争(如局部变量),要么加以同步控制。

竞态条件(race condition)是多线程编程中的一种典型问题。当多个线程并发访问同一资源,且至少有一个线程在写入时,如果没有适当的同步,就会出现竞态条件。此时程序行为取决于线程执行的时序,可能导致不可预期的错误。例如,两个线程同时执行 counter++(这是一个非原子操作),可能导致更新丢失。

死锁(deadlock)是指两个或多个线程互相等待对方持有的锁而都无法继续执行的情形。例如,线程 A 持有锁 L1 并等待锁 L2,而线程 B 持有锁 L2 并等待锁 L1,双方互相等待,程序陷入僵局。避免死锁的常见做法是保证获取多个锁的顺序一致,或使用超时锁/可中断锁等机制。

示例:考虑以下竞态示例:

class Counter {
    private int count = 0;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

public class RaceConditionDemo {
    public static void main(String[] args) throws InterruptedException {
        Counter c = new Counter(); // 启动两个线程并发执行 increment() 
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                c.increment();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                c.increment();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(c.getCount()); // 结果可能小于 20000,说明发生了竞态条件 
    }
}

在上例中,由于 increment() 方法未同步,多线程同时执行 count++ 操作会导致最终 count 小于预期(发生竞态)。

死锁示例(示意):

Object lockA = new Object();
Object lockB = new Object();
Thread t1 = new Thread(() -> {
    synchronized (lockA) {
        // 线程1 已持有 lockA 
        synchronized (lockB) {
            // ... 
        }
    }
});
Thread t2 = new Thread(() -> {
    synchronized (lockB) {
        // 线程2 已持有 lockB 
        synchronized (lockA) {
            // ... 
        }
    }
}); 
// 这种情况下 t1 和 t2 会互相等待,发生死锁:contentReference[oaicite:35]{index=35}。
t1.start(); 
t2.start();

上例中,线程 t1 和 t2 分别持有一个锁后又试图获取对方锁,导致互相等待直至死锁。

小结

Java 多线程编程通过线程并发执行能提高程序效率,但也带来同步问题。本文从线程概念、创建方式、生命周期、同步机制等方面系统介绍了 Java 多线程的基础知识,并结合流程图和实例进行了说明。初学者应关注线程安全和同步机制(如 synchronizedReentrantLockvolatile)的使用,以正确地控制并发执行,避免竞态条件和死锁等问题,从而编写可靠的并发程序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值