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类
步骤:
- 创建一个继承Thread类的子类
- 重写run()方法,编写线程执行逻辑
- 创建子类实例并调用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接口
步骤:
- 创建一个实现Runnable接口的类
- 实现run()方法,编写线程执行逻辑
- 创建Runnable实现类的实例
- 创建Thread对象,将Runnable实例作为参数传入
- 调用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的对比
| 特性 | synchronized | Lock |
|---|---|---|
| 锁的获取与释放 | 自动获取和释放 | 手动获取和释放(必须在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 避免死锁
死锁的四个必要条件:
- 互斥条件:资源不能被共享
- 请求与保持条件:线程已持有至少一个资源,但又提出新的资源请求
- 不可剥夺条件:已获得的资源在未使用完之前不能被其他线程强行夺走
- 循环等待条件:若干线程之间形成头尾相接的循环等待资源关系
避免死锁的方法:
- 破坏四个必要条件中的任意一个
- 按顺序申请资源
- 设置超时时间
- 使用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多线程的基本概念、创建和管理线程的方法、线程同步机制、线程池的使用以及并发工具类等内容。在实际开发中,合理利用多线程可以显著提高程序的性能和响应速度,但也需要注意线程安全问题,避免常见的陷阱和错误。