初识多线程(线程的状态与线程安全)
在并发编程中,多线程是提升程序效率的重要手段,但也伴随着线程安全等问题。本文将从线程状态、线程安全风险及解决方案等方面,带你初识多线程的核心基础知识。
一、线程的状态
线程在生命周期中会经历多种状态,Java 中线程的状态主要分为以下几种,其中与"等待"相关的状态需要重点区分:
- NEW:线程对象已创建,但尚未调用 start() 方法(未启动)。
- RUNNABLE:线程正在 Java 虚拟机中执行,或等待操作系统调度(如等待 CPU 时间片),处于可运行状态。
- BLOCKED:表示线程正在等待获取锁(如进入
synchronized代码块时未获取到锁)。 - WAITING:表示线程在无限期等待其他线程的通知(如调用
wait()、join()等方法后),需其他线程主动唤醒(如调用notify())。 - TIMED_WAITING:表示线程在有限时间内等待唤醒(如调用
sleep(1000)、wait(1000)等方法),超时后会自动唤醒。 - TERMINATED:线程已执行完毕(run()方法退出),生命周期结束
示例:观察线程状态转换
public class ThreadStateDemo {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
try {
// 进入TIMED_WAITING状态(超时等待)
Thread.sleep(1000);
// 进入WAITING状态(无限等待)
synchronized (ThreadStateDemo.class) {
ThreadStateDemo.class.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 新建状态
System.out.println("线程启动前状态:" + t.getState()); // NEW
t.start();
Thread.sleep(100);
System.out.println("线程休眠时状态:" + t.getState()); // TIMED_WAITING
Thread.sleep(1000);
System.out.println("线程等待时状态:" + t.getState()); // WAITING
}
}
二、多线程带来的风险——线程安全(重点)
多线程并发执行时,若不加以控制,可能导致线程安全问题。我们先从现象入手,逐步分析本质。
1. 观察线程不安全
最经典的例子是多线程同时修改共享变量:
public class ThreadUnsafeDemo {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
// 两个线程同时对count自增10000次
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
count++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("最终结果:" + count); // 预期20000,实际可能小于20000
}
}
现象:最终结果往往小于20000。原因是count++并非原子操作,多线程并发时会出现"丢失更新"。
2. 线程安全的概念
线程安全是指:在多线程环境下,无论线程调度方式如何,程序都能保证正确的执行结果(与单线程执行结果一致)。
3. 线程不安全的原因
a. 线程调度是随机的
操作系统对线程的调度是抢占式的,执行顺序不可预测。这是线程安全问题的根本原因,使得多线程执行路径存在无数种可能。
b. 修改共享数据
多个线程同时读写同一份共享数据(如上述例子中的count),若缺乏同步控制,会导致数据不一致。
c. 原子性问题
原子性是指:一个操作或一组操作,要么全部执行且执行过程不被中断,要么全不执行。
例如count++可拆分为3步:
- 读取
count的当前值; - 对值进行+1运算;
- 将结果写回
count。
多线程并发时,步骤可能被穿插执行,导致结果错误。
解决原子性问题:可使用java.util.concurrent.atomic包下的原子类(如AtomicInteger),其内部通过CAS(Compare-And-Swap)机制保证原子操作:
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicDemo {
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
count.incrementAndGet(); // 原子自增
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
count.incrementAndGet();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("最终结果:" + count); // 稳定输出20000
}
}
d. 指令重排序
编译器或CPU为优化性能,可能会调整指令的执行顺序。单线程下不影响结果,但多线程下可能导致逻辑错误。
例如:
// 线程1
int a = 1; // 步骤1
boolean flag = true; // 步骤2
// 线程2
if (flag) {
System.out.println(a); // 可能输出0(因步骤1和步骤2被重排序)
}
e. 可见性问题
CPU缓存导致线程对共享变量的修改,其他线程可能无法立即感知(即"不可见")。
例如:
public class VisibilityDemo {
private static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (!flag) {
// 循环等待flag变为true
}
System.out.println("线程退出");
});
t.start();
Thread.sleep(1000);
flag = true; // 主线程修改flag
}
}
现象:线程可能永远不退出,因为flag的修改未被线程t感知(线程t一直读取缓存中的旧值)。
4. 解决方法
(1)synchronized关键字
synchronized是Java中最常用的同步机制,核心特性:
- 互斥性:同一时间只有一个线程能执行
synchronized修饰的代码块(或方法),其他线程需等待锁释放。 - 可重入性:同一线程可多次获取同一把锁(避免自身死锁)。
使用方式:
// 1. 修饰代码块(锁对象自定义)
synchronized (lockObject) {
// 临界区代码
}
// 2. 修饰实例方法(锁对象为this)
public synchronized void method() {
// 临界区代码
}
// 3. 修饰静态方法(锁对象为类对象,如ClassName.class)
public static synchronized void staticMethod() {
// 临界区代码
}
用synchronized解决线程不安全问题:
public class SynchronizedDemo {
private static int count = 0;
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
synchronized (lock) { // 加锁保证原子性
count++;
}
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
synchronized (lock) {
count++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("最终结果:" + count); // 稳定输出20000
}
}
(2)volatile关键字
volatile主要解决可见性和禁止指令重排序问题,但不保证原子性。
- 可见性:一个线程对
volatile变量的修改,会立即被其他线程感知(强制刷新CPU缓存)。 - 禁止重排序:
volatile变量的读写操作前后的指令不会被重排序。
用volatile解决可见性问题:
public class VolatileDemo {
private static volatile boolean flag = false; // 用volatile修饰
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (!flag) {
// 循环等待
}
System.out.println("线程退出"); // 能正常退出
});
t.start();
Thread.sleep(1000);
flag = true; // 修改后立即被线程t感知
}
}
注意:volatile与synchronized的区别:
synchronized保证原子性、可见性、有序性(通过互斥)。volatile仅保证可见性和有序性,不保证原子性。
5. Java标准库中的线程安全类
Java标准库中部分类天生线程安全,主要分为两类:
-
通过加锁实现线程安全:
ConcurrentHashMap:高效的并发哈希表(重点),采用分段锁机制,支持高并发读写。Hashtable:全表加锁,并发效率低(已被ConcurrentHashMap替代)。Vector、Stack等(加锁的集合类,效率较低)。
-
不可变对象(无修改操作):
String:字符串一旦创建不可修改,多线程读取安全。Integer、Long等包装类(值不可变)。
总结
本文介绍了多线程的核心基础知识:
- 线程状态中
BLOCKED、WAITING、TIMED_WAITING的区别。 - 线程安全的本质是多线程对共享数据的并发修改,根源在于线程调度的随机性,辅以原子性、可见性、有序性问题。
- 解决线程安全的关键手段:
synchronized(保证原子性、可见性、有序性)和volatile(保证可见性、有序性)。 - 标准库中线程安全类的特点及使用场景。
多线程编程的核心是"控制并发",理解线程安全的本质和解决方案,是编写可靠并发程序的基础。
更多推荐
所有评论(0)