一、认识线程
1. 什么是线程
线程就是一条独立执行流 ,多个线程可以同时执行多份代码。 进程是资源分配的最小单位;线程是CPU调度的最小单位。
- 一个进程至少包含1个主线程;
- 同一个进程内多个线程共享堆、方法区内存;每个线程私有虚拟机栈、本地方法栈、程序计数器;
- 进程之间内存相互隔离;一个线程崩溃,会导致整个进程崩溃。
为什么用线程:
- 充分利用多核CPU算力;
- IO等待时可以执行其他任务,提升程序整体效率;
- 相比进程,线程创建、销毁、调度开销更小。
2. Java创建线程的4种写法
- 继承Thread类 ,重写
run(),调用start()启动线程
java
class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程执行");
}
}
// 使用
MyThread t = new MyThread();
t.start();
- 实现Runnable接口,传入Thread构造器
java
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("runnable执行");
}
}
Thread t = new Thread(new MyRunnable());
t.start();
- 匿名内部类(Thread)
java
Thread t = new Thread(){
@Override
public void run() {}
};
- Lambda表达式(推荐,代码简洁)
java
Thread t = new Thread(() -> {
System.out.println("lambda线程");
});
t.start();
⚠️注意:
run()只是普通方法,start()才会向操作系统申请创建真正的线程。
3. Thread常用属性与方法
| API | 说明 |
|---|---|
getId() |
线程唯一id |
getName() |
获取线程名字 |
getState() |
获取线程状态 |
getPriority() |
获取优先级,优先级高只是更容易被调度,不代表一定先执行 |
isDaemon() |
是否后台守护线程,JVM等待所有非后台线程结束才退出 |
isAlive() |
线程是否存活:不是NEW、不是TERMINATED即为存活 |
interrupt() |
中断线程,设置中断标记;如果线程阻塞(sleep/wait)会抛出InterruptedException并清除标记 |
currentThread() |
静态方法,获取当前正在执行的线程对象 |
sleep(ms) |
休眠线程,不会释放锁;实际休眠时间≥设置时间,受操作系统调度影响 |
join() |
等待其他线程执行完毕,支持设置最大等待时间 |
中断线程两种方式:
- 自定义
volatile布尔标记位,循环判断标记退出; interrupt()+isInterrupted()/Thread.interrupted()。interrupted():读取中断标记同时清除标记 ;isInterrupted()只读取,不清除标记。
二、线程的6种状态与流转
线程状态是枚举Thread.State,一共6种:
- NEW :线程对象创建,还没调用
start() - RUNNABLE:就绪/运行中,代表线程可以被CPU调度执行(不一定正在跑CPU)
- BLOCKED :等待获取
synchronized锁,锁被别的线程占有,阻塞排队 - WAITING :无限等待,等待其他线程唤醒(
wait()、join()、park()) - TIMED_WAITING :限时等待,超时自动唤醒(
sleep()、wait(time)) - TERMINATED:线程执行完毕
区分:
BLOCKED:抢锁失败,等锁;WAITING / TIMED_WAITING:主动放弃CPU,等待其他线程通知或者超时。
三、线程安全
定义:多线程环境下,程序运行结果和单线程预期结果一致,就是线程安全。
线程不安全的4个根源
- 线程抢占式随机调度:操作系统随时切换线程,执行顺序不可预测;
- 修改共享变量:多个线程同时读写同一个内存变量;
- 不具备原子性 :一条Java代码可能对应多条CPU指令,执行中途会被打断。 例如
count++分为三步:读内存 → CPU计算 → 写回内存,多线程并发会出现数据丢失; - 内存可见性问题:JMM Java内存模型,线程拥有自己的工作内存(CPU缓存/寄存器),修改先写副本,不一定立刻刷入主内存,其他线程看不到最新值;
- 指令重排序:编译器、CPU为性能对指令进行重排,单线程逻辑不变,多线程下会出现异常。
Java内存模型 JMM
- 主内存:真正物理内存,存放共享变量;
- 工作内存:每个线程私有,对应CPU寄存器、高速缓存,是变量副本。 线程读写共享变量,都是操作工作内存,再同步主内存,会带来可见性bug。
四、synchronized 监视器锁
synchronized是Java内置的悲观锁,基于操作系统mutex实现。
三大特性
- 互斥(原子性):同一时间只允许一个线程进入同步块。进入=加锁,退出=解锁;竞争失败线程进入阻塞队列BLOCKED。
- 可重入:同一个线程可以重复获取同一把锁;内部维护锁持有者+计数器,计数器归0才释放锁,避免自己把自己锁死。
- 保证可见性、禁止指令重排序。
四种写法
- 锁普通对象代码块:
synchronized(对象){} - 锁当前对象this:
synchronized(this){} - 修饰普通方法:锁当前实例对象
java
public synchronized void test(){}
- 修饰静态方法:锁类对象(
.class)
java
public static synchronized void test(){}
关键点:只有竞争同一对象锁,才会阻塞;不同对象互不干扰。
JDK线程安全/不安全类
- ❌线程不安全:
ArrayList、HashMap、HashSet、StringBuilder - ✅线程安全:
StringBuffer、Vector、HashTable(不推荐)、ConcurrentHashMapString不可变,天生线程安全。
五、volatile关键字
作用:保证变量内存可见性,禁止指令重排序;不保证原子性。
- 写volatile变量:修改工作内存副本,强制刷新到主内存;
- 读volatile变量:强制从主内存读取最新值,不使用缓存副本。
注意:
volatile int count不能解决count++线程不安全,因为++不是原子操作。使用场景:多线程下一个线程写、多个线程读的标记变量。
六、wait / notify / notifyAll
属于Object的方法,用来做线程之间的协作通信,控制线程执行顺序。
wait()
- 当前线程释放锁,进入对象等待队列;
- 等待被唤醒、超时、被interrupt;
- 必须写在synchronized同步块内部,否则抛异常。
notify()
唤醒该对象等待队列中随机一个线程;不会立刻释放锁,退出同步代码块之后锁才释放。
notifyAll()
唤醒该对象等待队列全部等待线程;唤醒后所有线程需要重新竞争锁。
wait vs sleep对比
wait sleep 归属 Object方法 Thread静态方法 锁 必须在synchronized内调用,调用后释放锁 不需要锁,休眠不释放锁 用途 线程间通信协作 单纯让线程休眠一段时间
最佳实践:
wait()要写在while循环里面,防止虚假唤醒。
七、多线程经典案例
案例1:单例模式
- 饿汉模式:类加载直接创建实例,天然线程安全
java
class Singleton{
private static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
- 懒汉模式(双重检查锁DCL)
java
class Singleton{
// volatile禁止指令重排序,防止半初始化对象
private static volatile Singleton instance;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){ //外层判断减少锁竞争
synchronized (Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
volatile作用:阻止new Singleton()指令重排,避免其他线程拿到未初始化完成的对象。
案例2:阻塞队列 & 生产者消费者模型
阻塞队列特性:
- 队列满时,入队
put()阻塞; - 队列空时,出队
take()阻塞。
生产者‑消费者模型作用:
- 解耦:生产者、消费者不直接交互,通过队列通信;
- 削峰填谷:缓冲流量,平衡生产和消费速度,防止瞬时流量打垮服务。
标准实现:LinkedBlockingQueue,put()入队,take()阻塞取出。
简易实现思路:数组存储元素,synchronized + wait/notifyAll;put满就wait;take空就wait。
案例3:定时器Timer
- JDK提供
Timer,schedule(task,delay)延迟执行任务。 - 简易定时器实现:优先队列存放定时任务,后台线程循环扫描队首任务,时间到执行任务。
案例4:线程池
频繁创建销毁线程开销高,线程池复用已有线程。 核心组成:核心线程数、最大线程数、阻塞队列、空闲存活时间、拒绝策略。
Executors工具快速创建线程池:
newFixedThreadPool(n)固定线程数newCachedThreadPool()动态扩容newSingleThreadExecutor()单线程池
四种拒绝策略:
AbortPolicy:直接抛异常(默认)CallerRunsPolicy:调用者线程自己执行任务DiscardOldestPolicy:丢弃队列最老任务DiscardPolicy:直接丢弃新任务,不抛异常
八、保证线程安全的整体思路
- 不使用共享变量;
- 共享变量只读,使用不可变对象;
- 修改共享变量时:保证原子性、可见性、有序性
- 原子性:
synchronized、锁、CAS; - 可见性:
volatile、锁; - 有序性:
volatile、锁禁止重排序。
- 原子性:
结尾小结
多线程核心难点在于理解:抢占调度带来执行顺序不可控,引出原子性、可见性、重排序三类问题。
synchronized悲观锁,保证原子+可见+有序,会阻塞;volatile只解决可见性与重排序,不能保证原子性;wait‑notify做线程通信;- 业务优先使用线程池,不要手动频繁new Thread。