一、认识线程
1.1 线程的概念
线程是操作系统中的执行流,多个线程可以 "同时" 执行多份代码。就像公司办理业务时,多个会计分别处理不同任务,共同完成公司的整体业务。其中,启动其他线程的线程称为主线程(Main Thread)。
1.2 为什么需要线程
- 充分利用多核 CPU:单核 CPU 发展遇到瓶颈,多核 CPU 需要并发编程来提高算力
- 处理 IO 等待:在等待 IO 操作时,可以利用线程处理其他工作
- 轻量级:相比进程,线程的创建、销毁和调度成本更低
1.3 进程与线程的区别
特性 | 进程 | 线程 |
---|---|---|
资源分配 | 系统分配资源的最小单位 | 系统调度的最小单位 |
内存空间 | 进程间不共享内存 | 同一进程的线程共享内存 |
独立性 | 进程崩溃不影响其他进程 | 线程崩溃可能导致整个进程崩溃 |
包含关系 | 包含线程,至少有一个主线程 | 属于进程 |
1.4 Java 线程与操作系统线程的关系
Java 中的Thread
类是对操作系统线程 API 的封装和抽象,操作系统内核实现了线程机制并提供 API(如 Linux 的 pthread 库),Java 通过Thread
类简化了线程操作。
二、创建线程的方法
2.1 继承 Thread 类
class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程运行的代码");
}
}
// 使用
MyThread t = new MyThread();
t.start(); // 启动线程
2.2 实现 Runnable 接口
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程运行的代码");
}
}
// 使用
Thread t = new Thread(new MyRunnable());
t.start(); // 启动线程
2.3 其他创建方式
- 匿名内部类创建 Thread 子类对象
- 匿名内部类创建 Runnable 子类对象
- Lambda 表达式创建 Runnable 对象
// Lambda表达式示例
Thread t = new Thread(() -> System.out.println("使用Lambda创建线程"));
t.start();
三、Thread 类的常见方法
3.1 构造方法
方法 | 说明 |
---|---|
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用 Runnable 对象创建线程 |
Thread(String name) | 创建指定名称的线程 |
Thread(Runnable target, String name) | 使用 Runnable 对象创建指定名称的线程 |
3.2 常用属性及获取方法
属性 | 获取方法 | 说明 |
---|---|---|
ID | getId() | 线程唯一标识 |
名称 | getName() | 线程名称,用于调试 |
状态 | getState() | 线程当前状态 |
优先级 | getPriority() | 优先级高的线程更容易被调度 |
后台线程 | isDaemon() | JVM 在所有非后台线程结束后退出 |
是否存活 | isAlive() | 线程的 run () 方法是否运行结束 |
是否中断 | isInterrupted() | 线程中断状态 |
3.3 关键方法
- start():启动线程,使线程开始执行
- run():线程执行的任务代码
- interrupt():中断线程
- join():等待线程执行完毕
- currentThread():获取当前线程对象
- sleep(long millis):使当前线程休眠指定毫秒数
四、线程状态及转换
Java 线程有 6 种状态,定义在Thread.State
枚举中:
- NEW:线程已创建但未启动
- RUNNABLE:可运行状态,包括正在运行和就绪状态
- BLOCKED:阻塞状态,等待获取锁
- WAITING:无限期等待状态
- TIMED_WAITING:限时等待状态
- TERMINATED:线程执行完毕
状态转换关系:
- 调用
start()
方法:NEW → RUNNABLE - 线程执行完毕:RUNNABLE → TERMINATED
- 调用
sleep()
或wait(timeout)
:RUNNABLE → TIMED_WAITING - 调用
wait()
:RUNNABLE → WAITING - 等待锁时:RUNNABLE → BLOCKED
五、线程安全问题
5.1 线程不安全的原因
- 线程调度随机性:多线程执行顺序不确定
- 共享数据修改:多个线程修改同一变量
- 原子性问题:操作不是原子的,可能被中断
- 可见性问题:一个线程的修改不能被其他线程及时看到
- 指令重排序:编译器优化导致指令执行顺序变化
5.2 解决线程安全问题
5.2.1 synchronized 关键字
synchronized
保证原子性,提供互斥访问:
// 修饰代码块
synchronized (lockObject) {
// 需要同步的代码
}
// 修饰方法
public synchronized void method() {
// 需要同步的代码
}
synchronized
特性:
- 互斥性:同一时间只有一个线程能执行同步代码
- 可重入性:同一线程可多次获取同一把锁
- 自动释放:代码块执行完毕自动释放锁
5.2.2 volatile 关键字
volatile
保证内存可见性,使变量修改能被其他线程及时看到:
private volatile int count = 0;
注意:volatile
不保证原子性,仅解决可见性问题。
六、锁策略
6.1 乐观锁 vs 悲观锁
- 悲观锁:假设冲突会发生,每次操作都加锁
- 乐观锁:假设冲突很少发生,操作时不加锁,提交时检查冲突
synchronized
初始使用乐观锁,冲突频繁时转为悲观锁。
6.2 重量级锁 vs 轻量级锁
- 重量级锁:依赖操作系统 mutex,涉及用户态与内核态切换
- 轻量级锁:在用户态完成加锁,冲突严重时升级为重量级锁
6.3 自旋锁
获取锁失败时不阻塞,而是循环尝试获取锁,适用于锁持有时间短的场景。
6.4 公平锁 vs 非公平锁
- 公平锁:按请求顺序获取锁
- 非公平锁:不按顺序,允许 "插队"
synchronized
是非公平锁,ReentrantLock
可通过构造参数指定公平性。
6.5 读写锁
分离读操作和写操作,允许多个读操作同时进行,读与写、写与写互斥。Java 中通过ReentrantReadWriteLock
实现。
七、CAS 操作
7.1 什么是 CAS
CAS(Compare and Swap,比较并交换)是一种原子操作,包含三个操作数:内存地址、预期值和新值。只有当内存中的值等于预期值时,才将新值写入该地址。
7.2 CAS 的应用
- 实现原子类:如
AtomicInteger
- 实现自旋锁
7.3 ABA 问题
CAS 无法区分值是一直未变还是变化后又变回原值。解决方法是引入版本号,Java 中可使用AtomicStampedReference
。
八、JUC 常用类
8.1 Callable 与 FutureTask
Callable
用于描述有返回值的任务,FutureTask
用于获取任务结果:
Callable<Integer> callable = () -> {
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
return sum;
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);
new Thread(futureTask).start();
int result = futureTask.get(); // 获取结果,会阻塞
8.2 ReentrantLock
可重入锁,与synchronized
相比更灵活:
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 临界区
} finally {
lock.unlock(); // 必须手动释放锁
}
8.3 线程池
线程池管理线程生命周期,减少创建销毁开销:
// 创建固定大小的线程池
ExecutorService pool = Executors.newFixedThreadPool(10);
// 提交任务
pool.submit(() -> System.out.println("执行任务"));
// 关闭线程池
pool.shutdown();
ThreadPoolExecutor
构造参数详解:
corePoolSize
:核心线程数maximumPoolSize
:最大线程数keepAliveTime
:非核心线程空闲时间workQueue
:任务队列threadFactory
:线程工厂handler
:拒绝策略
8.4 其他工具类
- Semaphore:信号量,控制并发访问数量
- CountDownLatch:等待多个任务完成
九、死锁及避免
9.1 死锁产生的条件
- 互斥使用
- 不可抢占
- 请求和保持
- 循环等待
9.2 避免死锁的方法
最有效的方法是破坏循环等待条件,例如对锁进行编号,按固定顺序获取锁。
十、线程安全的集合类
10.1 常用线程安全集合
ConcurrentHashMap
:线程安全的 HashMapCopyOnWriteArrayList
:读多写少场景的线程安全 List- 阻塞队列:
ArrayBlockingQueue
、LinkedBlockingQueue
等
10.2 ConcurrentHashMap 特点
- 读操作不加锁,使用 volatile 保证可见性
- 写操作针对每个哈希桶加锁,降低锁冲突
- 优化扩容方式,提高并发性能
总结
多线程编程是 Java 开发中的重要知识点,掌握线程的创建、同步机制、锁策略及 JUC 工具类的使用,能够编写高效且线程安全的并发程序。在实际开发中,需根据具体场景选择合适的并发策略,避免线程安全问题和性能瓶颈。