多线程与并发编程是Java开发中至关重要的高级特性,合理使用可以显著提升程序性能,但若使用不当则会导致各种难以调试的问题。本文将系统性地介绍Java多线程的核心概念和最佳实践,帮助开发者掌握这一强大工具。

一、多线程基础实现

1.1 继承Thread类

继承Thread类是最基础的多线程实现方式:

public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("线程执行: " + Thread.currentThread().getName());
    }
    
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        t1.start();  // 启动线程1
        t2.start();  // 启动线程2
    }
}

关键点:

  • 必须重写run()方法

  • 通过start()方法启动线程,而非直接调用run()

  • 每个线程都有独立的调用栈

1.2 实现Runnable接口

更推荐的实现方式,因为Java不支持多重继承:

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Runnable执行: " + Thread.currentThread().getName());
    }
    
    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable());
        Thread t2 = new Thread(new MyRunnable());
        t1.start();
        t2.start();
    }
}

优势:

  • 避免单继承限制

  • 更适合资源共享

  • 与线程池等高级特性配合更好

1.3 Java 8 Lambda简化写法

new Thread(() -> {
    System.out.println("Lambda线程: " + Thread.currentThread().getName());
}).start();

二、线程同步与线程安全

2.1 synchronized关键字

方法同步:

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

代码块同步:

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

静态方法同步:

public static synchronized void staticIncrement() {
    staticCount++;
}

2.2 Lock接口及其实现

Java 5引入了更灵活的java.util.concurrent.locks.Lock接口:

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

public class Counter {
    private final Lock lock = new ReentrantLock();
    private int count = 0;
    
    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }
}

Lock vs synchronized:

  • Lock更灵活,可以尝试获取锁(tryLock)

  • Lock可以设置公平性

  • Lock提供了更丰富的功能(如Condition)

  • synchronized更简洁,自动释放锁

2.3 volatile关键字

private volatile boolean running = true;

public void stop() {
    running = false;
}

适用场景:

  • 保证变量的可见性

  • 不保证原子性(不适合计数器场景)

  • 比synchronized更轻量

三、线程池与Executor框架

3.1 为什么需要线程池

  • 降低资源消耗(减少线程创建销毁开销)

  • 提高响应速度(任务到达时线程已存在)

  • 提高线程可管理性(统一分配、调优和监控)

3.2 Executor框架核心类

1. 创建固定大小线程池:

ExecutorService fixedPool = Executors.newFixedThreadPool(5);

2. 创建可缓存线程池:

ExecutorService cachedPool = Executors.newCachedThreadPool();

3. 创建单线程池:

ExecutorService singleThread = Executors.newSingleThreadExecutor();

4. 创建定时任务线程池:

ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(3);

3.3 ThreadPoolExecutor详解

实际开发中建议直接使用ThreadPoolExecutor构造函数:

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    5, // 核心线程数
    10, // 最大线程数
    60L, // 空闲线程存活时间
    TimeUnit.SECONDS, // 时间单位
    new ArrayBlockingQueue<>(100) // 工作队列
);

关键参数解析:

  • 核心线程数:池中保持的线程数量(即使空闲)

  • 最大线程数:池中允许的最大线程数量

  • 存活时间:超过核心线程数的空闲线程存活时间

  • 工作队列:用于保存待执行任务的阻塞队列

  • 拒绝策略:当任务无法执行时的处理策略

3.4 线程池最佳实践

  1. 合理配置线程数:

    • CPU密集型:CPU核心数 + 1

    • IO密集型:CPU核心数 * (1 + 平均等待时间/平均计算时间)

  2. 使用有界队列:避免无界队列导致OOM

  3. 明确拒绝策略

    • AbortPolicy(默认):抛出RejectedExecutionException

    • CallerRunsPolicy:由调用线程执行该任务

    • DiscardPolicy:直接丢弃任务

    • DiscardOldestPolicy:丢弃队列最前面的任务

四、常见并发问题与解决方案

4.1 死锁与避免

死锁示例:

// 线程1
synchronized(resourceA) {
    synchronized(resourceB) {
        // 操作
    }
}

// 线程2
synchronized(resourceB) {
    synchronized(resourceA) {
        // 操作
    }
}

避免策略:

  • 按固定顺序获取锁

  • 使用tryLock设置超时

  • 使用更高级别的并发工具

4.2 线程间通信

wait/notify机制:

public class SharedResource {
    private boolean ready = false;
    
    public synchronized void produce() {
        ready = true;
        notifyAll();
    }
    
    public synchronized void consume() throws InterruptedException {
        while(!ready) {
            wait();
        }
        // 消费逻辑
    }
}

更现代的替代方案:

  • BlockingQueue

  • CountDownLatch

  • CyclicBarrier

  • Semaphore

五、Java并发工具包(java.util.concurrent)

5.1 原子类(Atomic)

AtomicInteger counter = new AtomicInteger(0);

// 线程安全的自增
counter.incrementAndGet();

5.2 Concurrent集合

  • ConcurrentHashMap:线程安全的HashMap

  • CopyOnWriteArrayList:读多写少的List

  • BlockingQueue:阻塞队列实现

5.3 CountDownLatch

CountDownLatch latch = new CountDownLatch(3);

// 工作线程
new Thread(() -> {
    // 工作
    latch.countDown();
}).start();

// 主线程等待
latch.await();

六、最佳实践总结

  1. 优先使用高级并发工具:而不是直接使用wait/notify

  2. 尽量使用不可变对象:避免同步问题

  3. 缩小同步范围:只同步必要的代码块

  4. 文档化线程安全策略:明确类的线程安全级别

  5. 避免过度同步:可能导致性能问题或死锁

  6. 考虑使用并发集合:而非手动同步的集合

结语

多线程与并发编程是Java开发中的双刃剑,合理使用可以极大提升程序性能,使用不当则会导致各种难以调试的问题。通过本文的介绍,希望读者能够掌握Java多线程的核心概念和最佳实践。

在实际开发中,建议:

  1. 优先考虑使用java.util.concurrent包中的高级工具

  2. 谨慎使用低级别的同步机制

  3. 充分测试多线程代码

  4. 保持代码简洁明了

如果你觉得本文有帮助,请点赞收藏!对于多线程与并发编程,你还有哪些疑问或经验分享?欢迎在评论区留言讨论!

Logo

欢迎加入我们的广州开发者社区,与优秀的开发者共同成长!

更多推荐