Java多线程教程

44 阅读12分钟

Java多线程教程

一、多线程基础概念

1.1 什么是线程

线程是程序执行的最小单元,是进程中的一个独立执行路径。一个进程可以包含多个线程,这些线程共享进程的资源(如内存空间、文件句柄等),但每个线程有自己的程序计数器、栈内存等。

1.2 为什么需要多线程

  • 提高程序吞吐量:多核CPU可以并行执行多个线程
  • 提高响应速度:耗时操作可以在后台线程中执行
  • 充分利用CPU资源:避免单线程阻塞导致CPU空闲
  • 简化程序设计:某些场景下多线程模型更符合问题本质

1.3 线程的生命周期

线程的生命周期包含以下几个状态:

  • 新建(New):线程对象被创建但尚未启动
  • 就绪(Runnable):线程对象调用start()方法后,等待CPU调度
  • 运行(Running):线程获得CPU资源,正在执行run()方法
  • 阻塞(Blocked):线程暂时停止执行,等待某个条件满足
  • 等待(Waiting):线程无限期等待,直到被唤醒
  • 超时等待(Timed Waiting):线程等待指定时间后自动唤醒
  • 终止(Terminated):线程执行完毕或发生异常而终止

二、创建线程的方式

2.1 继承Thread类

步骤

  1. 创建一个继承Thread类的子类
  2. 重写run()方法,编写线程执行逻辑
  3. 创建子类实例并调用start()方法启动线程

示例代码

// 继承Thread类创建线程
class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("线程执行: " + i + ", 线程名称: " + getName());
            try {
                Thread.sleep(100); // 休眠100毫秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

// 测试类
public class ThreadDemo {
    public static void main(String[] args) {
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();
        
        thread1.start(); // 启动线程1
        thread2.start(); // 启动线程2
    }
}

2.2 实现Runnable接口

步骤

  1. 创建一个实现Runnable接口的类
  2. 实现run()方法,编写线程执行逻辑
  3. 创建Runnable实现类的实例
  4. 创建Thread对象,将Runnable实例作为参数传入
  5. 调用Thread对象的start()方法启动线程

示例代码

// 实现Runnable接口创建线程
class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("线程执行: " + i + ", 线程名称: " + Thread.currentThread().getName());
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

// 测试类
public class RunnableDemo {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        
        Thread thread1 = new Thread(myRunnable, "线程1");
        Thread thread2 = new Thread(myRunnable, "线程2");
        
        thread1.start();
        thread2.start();
    }
}

2.3 两种方式的对比

方式优点缺点
继承Thread类编写简单,直接使用this关键字访问线程不能继承其他类(Java单继承限制)
实现Runnable接口更灵活,可共享资源,可继承其他类无法直接使用this获取线程对象,需要通过Thread.currentThread()

推荐:优先使用实现Runnable接口的方式,因为它更灵活,也符合面向接口编程的思想。

2.4 使用Callable和Future

特点

  • 可以返回执行结果
  • 可以抛出异常

示例代码

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

// 实现Callable接口创建线程
class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum += i;
        }
        return sum;
    }
}

// 测试类
public class CallableDemo {
    public static void main(String[] args) {
        MyCallable myCallable = new MyCallable();
        FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
        
        Thread thread = new Thread(futureTask);
        thread.start();
        
        try {
            // 获取线程执行结果
            Integer result = futureTask.get();
            System.out.println("计算结果: " + result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

三、线程同步机制

3.1 线程安全问题

当多个线程同时访问共享资源时,如果不进行同步控制,可能会导致数据不一致的问题。

示例:模拟售票系统的线程安全问题

class TicketSystem implements Runnable {
    private int tickets = 10; // 共享资源
    
    @Override
    public void run() {
        while (tickets > 0) {
            // 模拟售票延迟
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            System.out.println(Thread.currentThread().getName() + " 售出第 " + tickets + " 张票");
            tickets--;
        }
    }
}

public class UnsafeDemo {
    public static void main(String[] args) {
        TicketSystem ticketSystem = new TicketSystem();
        
        Thread t1 = new Thread(ticketSystem, "窗口1");
        Thread t2 = new Thread(ticketSystem, "窗口2");
        Thread t3 = new Thread(ticketSystem, "窗口3");
        
        t1.start();
        t2.start();
        t3.start();
    }
}

3.2 synchronized关键字

3.2.1 同步方法

将synchronized关键字添加到方法上,使该方法成为同步方法。

class SafeTicketSystem implements Runnable {
    private int tickets = 10;
    
    @Override
    public void run() {
        while (true) {
            if (sellTicket()) {
                break;
            }
        }
    }
    
    // 同步方法
    private synchronized boolean sellTicket() {
        if (tickets > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            System.out.println(Thread.currentThread().getName() + " 售出第 " + tickets + " 张票");
            tickets--;
            return false;
        }
        return true;
    }
}
3.2.2 同步代码块

使用synchronized关键字包围代码块,只对关键代码进行同步。

class SafeTicketSystem2 implements Runnable {
    private int tickets = 10;
    private final Object lock = new Object(); // 同步锁
    
    @Override
    public void run() {
        while (true) {
            synchronized (lock) { // 同步代码块
                if (tickets > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    
                    System.out.println(Thread.currentThread().getName() + " 售出第 " + tickets + " 张票");
                    tickets--;
                } else {
                    break;
                }
            }
        }
    }
}

3.3 Lock接口

Java 5引入了更灵活的Lock接口,提供了比synchronized更强大的功能。

示例代码

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

class SafeTicketSystem3 implements Runnable {
    private int tickets = 10;
    private final Lock lock = new ReentrantLock(); // 可重入锁
    
    @Override
    public void run() {
        while (true) {
            lock.lock(); // 加锁
            try {
                if (tickets > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    
                    System.out.println(Thread.currentThread().getName() + " 售出第 " + tickets + " 张票");
                    tickets--;
                } else {
                    break;
                }
            } finally {
                lock.unlock(); // 释放锁,必须放在finally块中
            }
        }
    }
}

3.4 synchronized与Lock的对比

特性synchronizedLock
锁的获取与释放自动获取和释放手动获取和释放(必须在finally中释放)
锁的类型可重入锁可重入锁,还支持公平锁和非公平锁
等待可中断不可中断可中断
锁的条件没有显式的条件提供Condition接口,可以实现更复杂的条件等待
性能低版本JDK中性能较差,高版本已优化性能更好,更灵活

四、线程协作机制

4.1 wait()、notify()和notifyAll()方法

这些方法是Object类的方法,用于线程间通信。

使用条件

  • 必须在同步代码块或同步方法中使用
  • 调用这些方法的对象必须是当前同步锁对象

示例:生产者-消费者模式

class SharedResource {
    private int value = 0;
    private boolean available = false;
    
    // 生产者方法
    public synchronized void produce(int newValue) throws InterruptedException {
        // 等待消费者消费
        while (available) {
            wait(); // 释放锁并等待
        }
        
        value = newValue;
        available = true;
        System.out.println("生产者生产: " + value);
        notify(); // 通知消费者
    }
    
    // 消费者方法
    public synchronized int consume() throws InterruptedException {
        // 等待生产者生产
        while (!available) {
            wait(); // 释放锁并等待
        }
        
        available = false;
        System.out.println("消费者消费: " + value);
        notify(); // 通知生产者
        return value;
    }
}

4.2 Condition接口

Condition是Lock接口的配套工具,可以实现更精细的线程控制。

示例:使用Condition实现生产者-消费者模式

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

class ConditionResource {
    private int value = 0;
    private boolean available = false;
    private final Lock lock = new ReentrantLock();
    private final Condition producerCondition = lock.newCondition();
    private final Condition consumerCondition = lock.newCondition();
    
    public void produce(int newValue) throws InterruptedException {
        lock.lock();
        try {
            while (available) {
                producerCondition.await(); // 生产者等待
            }
            
            value = newValue;
            available = true;
            System.out.println("生产者生产: " + value);
            consumerCondition.signal(); // 通知消费者
        } finally {
            lock.unlock();
        }
    }
    
    public int consume() throws InterruptedException {
        lock.lock();
        try {
            while (!available) {
                consumerCondition.await(); // 消费者等待
            }
            
            available = false;
            System.out.println("消费者消费: " + value);
            producerCondition.signal(); // 通知生产者
            return value;
        } finally {
            lock.unlock();
        }
    }
}

五、线程池

5.1 线程池的概念

线程池是一种线程管理技术,它预先创建一组线程,等待任务分配,避免了频繁创建和销毁线程的开销。

5.2 线程池的好处

  • 重用线程资源:避免频繁创建和销毁线程的开销
  • 控制线程数量:防止系统资源耗尽
  • 提高响应速度:任务到达时立即有线程处理
  • 管理线程生命周期:提供更灵活的线程管理机制

5.3 Executor框架

Java提供了Executor框架来简化线程池的使用。核心接口包括:

  • Executor:最基本的接口,定义了执行任务的方法
  • ExecutorService:扩展Executor,提供了管理线程池的方法
  • ThreadPoolExecutor:线程池的核心实现类
  • ScheduledExecutorService:用于执行定时任务

5.4 常见的线程池类型

5.4.1 FixedThreadPool

固定大小的线程池,核心线程数和最大线程数相同。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

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

只有一个线程的线程池,所有任务按顺序执行。

ExecutorService executorService = Executors.newSingleThreadExecutor();
5.4.3 CachedThreadPool

可缓存的线程池,线程数可以动态调整。

ExecutorService executorService = Executors.newCachedThreadPool();
5.4.4 ScheduledThreadPool

用于执行定时任务的线程池。

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ScheduledThreadPoolDemo {
    public static void main(String[] args) {
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);
        
        // 延迟3秒后执行任务
        scheduledExecutorService.schedule(() -> {
            System.out.println("延迟执行任务: " + System.currentTimeMillis());
        }, 3, TimeUnit.SECONDS);
        
        // 延迟1秒后,每2秒执行一次任务
        scheduledExecutorService.scheduleAtFixedRate(() -> {
            System.out.println("定时执行任务: " + System.currentTimeMillis());
        }, 1, 2, TimeUnit.SECONDS);
    }
}

5.5 ThreadPoolExecutor详解

核心参数

  • corePoolSize:核心线程数
  • maximumPoolSize:最大线程数
  • keepAliveTime:非核心线程的空闲存活时间
  • unit:时间单位
  • workQueue:工作队列
  • threadFactory:线程工厂
  • handler:拒绝策略

示例:自定义ThreadPoolExecutor

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class CustomThreadPoolDemo {
    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
            2, // 核心线程数
            5, // 最大线程数
            60, TimeUnit.SECONDS, // 非核心线程空闲存活时间
            new ArrayBlockingQueue<>(10), // 工作队列
            new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
        );
        
        // 提交任务
        for (int i = 0; i < 20; i++) {
            final int taskId = i;
            executor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + " 执行任务 " + taskId);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        
        executor.shutdown();
    }
}

六、并发工具类

6.1 CountDownLatch

用于协调多个线程的同步工具类,允许一个或多个线程等待其他线程完成操作。

示例

import java.util.concurrent.CountDownLatch;

public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(3); // 计数器初始值为3
        
        for (int i = 0; i < 3; i++) {
            final int workerId = i;
            new Thread(() -> {
                System.out.println("工人 " + workerId + " 开始工作");
                try {
                    Thread.sleep(2000); // 模拟工作时间
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("工人 " + workerId + " 完成工作");
                latch.countDown(); // 计数器减1
            }).start();
        }
        
        System.out.println("等待所有工人完成工作...");
        latch.await(); // 等待计数器变为0
        System.out.println("所有工作已完成,可以进行下一步操作");
    }
}

6.2 CyclicBarrier

可循环使用的同步屏障,让一组线程到达一个屏障点时被阻塞,直到最后一个线程到达才会继续执行。

示例

import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        CyclicBarrier barrier = new CyclicBarrier(3, () -> {
            System.out.println("所有人都到达了,开始会议!");
        });
        
        for (int i = 0; i < 3; i++) {
            final int personId = i;
            new Thread(() -> {
                System.out.println("人员 " + personId + " 正在前往会议室");
                try {
                    Thread.sleep((long) (Math.random() * 3000));
                    System.out.println("人员 " + personId + " 到达会议室");
                    barrier.await(); // 等待所有人员到达
                    System.out.println("人员 " + personId + " 开始发言");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

6.3 Semaphore

信号量,控制对共享资源的访问数量,常用于限制可以同时访问某个资源的线程数。

示例

import java.util.concurrent.Semaphore;

public class SemaphoreDemo {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(2); // 允许2个线程同时访问
        
        for (int i = 0; i < 5; i++) {
            final int carId = i;
            new Thread(() -> {
                try {
                    System.out.println("车辆 " + carId + " 等待进入停车场");
                    semaphore.acquire(); // 获取许可
                    System.out.println("车辆 " + carId + " 进入停车场");
                    Thread.sleep(2000); // 模拟停车时间
                    System.out.println("车辆 " + carId + " 离开停车场");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release(); // 释放许可
                }
            }).start();
        }
    }
}

6.4 ConcurrentHashMap

线程安全的HashMap实现,比Hashtable性能更好,支持并发读写。

示例

import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapDemo {
    public static void main(String[] args) {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
        
        // 多线程操作ConcurrentHashMap
        for (int i = 0; i < 10; i++) {
            final int threadId = i;
            new Thread(() -> {
                for (int j = 0; j < 100; j++) {
                    String key = "key" + (j % 10);
                    map.put(key, threadId * 100 + j);
                }
            }).start();
        }
        
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println("ConcurrentHashMap大小: " + map.size());
        map.forEach((key, value) -> {
            System.out.println(key + ": " + value);
        });
    }
}

七、线程安全与锁优化

7.1 线程安全的实现方法

  • 互斥同步:synchronized、Lock
  • 非阻塞同步:CAS(Compare And Swap)
  • 无同步方案:线程本地存储(ThreadLocal)、不可变对象

7.2 ThreadLocal的使用

ThreadLocal提供线程本地变量,每个线程都有自己独立的变量副本。

示例

public class ThreadLocalDemo {
    private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0; // 初始值
        }
    };
    
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                Integer value = threadLocal.get();
                value += 10;
                threadLocal.set(value);
                System.out.println(Thread.currentThread().getName() + " 的值: " + threadLocal.get());
                threadLocal.remove(); // 使用完毕后清理,避免内存泄漏
            }).start();
        }
    }
}

7.3 避免死锁

死锁的四个必要条件

  1. 互斥条件:资源不能被共享
  2. 请求与保持条件:线程已持有至少一个资源,但又提出新的资源请求
  3. 不可剥夺条件:已获得的资源在未使用完之前不能被其他线程强行夺走
  4. 循环等待条件:若干线程之间形成头尾相接的循环等待资源关系

避免死锁的方法

  • 破坏四个必要条件中的任意一个
  • 按顺序申请资源
  • 设置超时时间
  • 使用Lock的tryLock方法尝试获取锁

八、多线程最佳实践

8.1 线程安全的设计原则

  • 最小同步范围:只同步必要的代码块
  • 优先使用并发集合:如ConcurrentHashMap、CopyOnWriteArrayList等
  • 使用不可变对象:不可变对象天然线程安全
  • 避免使用ThreadLocal作为缓存:容易导致内存泄漏

8.2 性能优化建议

  • 减少锁的粒度
  • 使用无锁算法
  • 合理设置线程池大小
  • 避免线程长时间阻塞
  • 使用并行流处理大数据集

8.3 常见陷阱

  • 忽略异常处理
  • 线程泄漏
  • 过度同步
  • 错误的锁使用
  • 线程安全的误解

九、实战练习:多线程下载器

以下是一个简单的多线程下载器示例,展示多线程的实际应用:

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;

public class MultiThreadDownloader {
    private static final int THREAD_COUNT = 3; // 线程数量
    private static final String FILE_URL = "https://example.com/large-file.zip";
    private static final String SAVE_PATH = "/path/to/save/large-file.zip";
    
    public static void main(String[] args) throws Exception {
        URL url = new URL(FILE_URL);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("GET");
        conn.setConnectTimeout(5000);
        
        int fileLength = conn.getContentLength();
        conn.disconnect();
        
        // 创建文件并设置大小
        RandomAccessFile file = new RandomAccessFile(SAVE_PATH, "rw");
        file.setLength(fileLength);
        file.close();
        
        // 计算每个线程下载的文件块大小
        int blockSize = fileLength / THREAD_COUNT;
        
        // 启动多个线程下载
        for (int i = 0; i < THREAD_COUNT; i++) {
            int startIndex = i * blockSize;
            int endIndex = (i == THREAD_COUNT - 1) ? fileLength - 1 : (i + 1) * blockSize - 1;
            
            new DownloadThread(startIndex, endIndex, i).start();
        }
    }
    
    static class DownloadThread extends Thread {
        private int startIndex;
        private int endIndex;
        private int threadId;
        
        public DownloadThread(int startIndex, int endIndex, int threadId) {
            this.startIndex = startIndex;
            this.endIndex = endIndex;
            this.threadId = threadId;
        }
        
        @Override
        public void run() {
            try {
                URL url = new URL(FILE_URL);
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                conn.setRequestMethod("GET");
                conn.setConnectTimeout(5000);
                conn.setRequestProperty("Range", "bytes=" + startIndex + "-" + endIndex);
                
                InputStream inputStream = conn.getInputStream();
                RandomAccessFile file = new RandomAccessFile(SAVE_PATH, "rw");
                file.seek(startIndex);
                
                byte[] buffer = new byte[1024];
                int len;
                while ((len = inputStream.read(buffer)) != -1) {
                    file.write(buffer, 0, len);
                }
                
                System.out.println("线程 " + threadId + " 下载完成");
                
                inputStream.close();
                file.close();
                conn.disconnect();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

通过本教程的学习,你应该能够理解Java多线程的基本概念、创建和管理线程的方法、线程同步机制、线程池的使用以及并发工具类等内容。在实际开发中,合理利用多线程可以显著提高程序的性能和响应速度,但也需要注意线程安全问题,避免常见的陷阱和错误。