多线程
Java线程安全三要素
| 概念 | 实现 | |
|---|---|---|
| 原子性 | 一个操作要不完全执行,要完全不执行 | synchronized与atomic包 |
| 可见性 | 线程对共享资源的修改可以被其他线程及时看到 Java每个线程都有自己的工作内存,对外不可见,其他线程可能使用旧值 | synchronized与volatile |
| 有序性 | 程序的执行顺序为代码的书写顺序 | synchronized,volatile,内存屏障 |
线程状态
| 状态 | 说明 |
|---|---|
| NEW | 刚创建,还未启动 |
| RUNNABLE | 就绪状态(调用start(),等待调度)+运行状态 |
| BLOCKED | 阻塞状态,等待监视器锁 |
| WAITING | 等待状态,等待其他线程执行特定动作(如notify) |
| TIMED_WAITING | 具有指定等待时间的等待状态 |
| TERMINATED | 终止状态,线程执行完成 |
sleep()与wait()
| 特性 | sleep | wait |
|---|---|---|
| 所属类 | Thread类(静态方法) | Object类(实例方法) |
| 释放对象锁 | 不会,但会释放CPU时间片 | 会 |
| 使用条件 | 随时可用 | 再同步块或同步方法里可用 |
| 唤醒机制 | 超时唤醒 | 其他线程使用notify()方法 |
| 用途 | 暂停线程执行 | 线程之间协作 |
线程通信
notify()
基于监视锁机制的wait(),notify()与notifyAll()
notify():默认随机唤醒一个线程,不过不同JVM有不同实现notifyAll():所有线程退出wait状态,开始竞争锁,但只有一个能抢到
class SharedObject {
public synchronized void consumerMethod() throws InterruptedException {
while (/* 条件不满足 */) {
wait();
}
// 执行相应操作
}
public synchronized void producerMethod() {
// 执行相应操作
notify(); // 或者 notifyAll()
}
}
volatile
作用有两个:
- 保障变量对所有线程的可见性:对该变量的写操作均会直接更新在主存,而读操作会直接从主存获取。
- 禁止指令重排序优化:通过内存屏障来禁止特定类型的指令重排序
- 在volatile写操作前后分别插入
StoreStore和StoreLoad屏障 - 在volatile读操作后插入
LoadLoad和LoadStore屏障 - 这些屏障会禁止特定类型的指令重排序,确保volatile写之前的操作不会被重排序到写之后,volatile读之后的操作不会被重排序到读之前
- 这建立了happens-before关系:操作A happen-before于 操作B,其实就是说 发生操作B之前,操作A产生的影响能被操作B观察到, 影响包括:修改了内存中共享变量的值、发送了消息、调用了方法等。
- 在volatile写操作前后分别插入
AQS
AbstractQueuedSynchronizer,用于构建锁,同步类的工具类
核心思想:
- 如果请求的共享资源空闲:就将当前请求线程设为有效的工作线程,锁定共享资源。
- 如果不空闲:就设置一定的阻塞等待机制来保证锁分配;使用双向列表实现,将活不到锁的线程加入队列中。
state:使用一个Volatile的int类型的成员变量来表示同步状态,通过CAS完成对State值的修改。这里state的具体含义,会根据具体实现类的不同而不同:- 在
Semapore里,表示剩余许可证的数量; - 在
CountDownLatch里,表示还需要倒数的数量; - 在
ReentrantLock中,state用来表示“锁”的占有情况,包括可重入计数,当state的值为0的时候,标识该Lock不被任何线程所占有。
- 在
- CLH变体队列:通过内置的FIFO双向列表队列来完成资源获取的排队工作。
- 重写获取/释放等方法:
tryAcquire,tryRelease
CountDownLatch
同步辅助类,允许一个或多个线程等待其他线程完成操作。
通过一个计数器count实现,初始化为线程的数量,每有一个线程完成任务,调用countDown方法将计数器减一,计数器为零时,等待的线程可以继续执行。
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
int threadCount = 3;//计数器
CountDownLatch latch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
try {
// 执行任务
System.out.println(Thread.currentThread().getName() + " 完成任务");
} finally {
latch.countDown();//减少计数器
}
}).start();
}
latch.await();//进入等待,直到计数器为零
System.out.println("所有线程任务完成");
}
}
CyclicBarrier
让一组线程相互等待,等所有线程均达到屏障点之后,再一起执行。
可重复使用,即所有线程通过屏障之后,计数器重置,待下次使用。
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
public static void main(String[] args) {
int threadCount = 3;
CyclicBarrier barrier = new CyclicBarrier(threadCount, () -> {
System.out.println("所有线程都到达屏障点");
});//构造函数,指定参与的线程数量和到达屏障点后执行的操作
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
try {
// 执行任务
System.out.println(Thread.currentThread().getName() + " 到达屏障点");
barrier.await();//使当前线程等待,直到所有线程都到达屏障点。
// 继续执行后续任务
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}
Semaphore
计数信号量。可以控制同时访问特定资源的数量
Semaphore(int permits):构造函数,指定信号量的初始许可数量。acquire():获取一个许可,如果没有可用许可则阻塞。release():释放一个许可。
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
public static void main(String[] args) {
int permitCount = 2;
Semaphore semaphore = new Semaphore(permitCount);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " 获得许可");
// 执行任务
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
System.out.println(Thread.currentThread().getName() + " 释放许可");
}
}).start();
}
}
}
终止线程
| 方法 | 适用场景 | 注意事项 |
|---|---|---|
| 循环检测标志 | 简单无阻塞的逻辑 | 标志位使用volatile以保证可见性 while (running//标志位){} |
| 中断机制 | 可中断的阻塞 | while (!Thread.currentThread().isInterrupted()){} |
| 资源关闭 | 不可中断的阻塞 | serverSocket.close(); // 关闭资源使accept()抛出异常 |
锁
监视器锁(synchronized)
可用于方法或代码块,当代码进入时会获取关联对象锁
原理:
- 在使用synchronized关键字后,在编译时会在同步的代码块加上
monitorenter与monitorexit字节码指令。 - 执行
monitorenter指令时会尝试获得对象锁,锁的计数器加1,其他竞争者进入等待队列。 - 执行
monitorexit会让计数器减1,当计数器为0时释放锁。
锁升级(锁膨胀)
通过lock的标识判断锁的级别:
- 无锁(01):没有偏向锁,JDK1.6之后,默认在偏向延迟(JVM启动的多少秒之后)开启
- 偏向锁:第一个线程A访问同步块,进入偏向锁。通过CAS将线程ID写入
Markword,此后该线程可再次使用(与Markword中的ID进行对比)。若B进入时,A已经退出,则偏向为B;若A未退出,即有竞争时,状态取消,进入轻量级锁。 - 轻量级锁:当有竞争时,进入轻量级锁:B自旋等待A释放,通过CAS获得锁。
- 重量级锁:当有两个以上的线程竞争锁时,进入重量级锁:创建monitor对象。
锁清除
若JVM虚拟机检测不到某段代码能被共享或竞争,就会将这段代码的同步锁取消掉,从而提高程序性能。
锁粗化
自动将一段连续的加锁解锁过程来链接在一起,扩展成一个更大的锁。
锁自旋
通过自身循环来尝试获取锁,可以避免一些线程挂起与恢复操作。但容易消耗CPU资源
可重入锁(ReentrantLock)
同一个线程在获得锁之后再次重复获得该锁,不会造成阻塞
原理:线程持有锁的计数器
- 当一个线程第一次获取锁时,计数器会加1,表示该线程持有了锁。在此之后,如果同一个线程再次获取锁,计数器会再次加1。每次线程成功获取锁时,都会将计数器加1。
- 当线程释放锁时,计数器会相应地减1。只有当计数器减到0时,锁才会完全释放,其他线程才有机会获取锁。
读写锁(ReadWriteLock)
乐观锁(CAS)
认为竞争不总是发生
实现方法:Compare and Swap操作:当A为A时,改为B,否则操作失败。
CAS缺点:
- ABA问题:当查看时为A,更改时也为A,当两次操作之间,可能发生了改变,变成了B又变成A。解决方法为:在CAS中加上版本号或者标记,通过对比值和版本号来解决ABA问题。
- 自旋循环时间长,开销大。
公平锁
- 公平锁:多个线程按申请锁的顺序获得锁,在队列中进入休眠态(耗时)排队,位于队列头的线程才能获得锁。执行速度慢,吞吐量更小。
- 不公平锁:线程申请锁时,先通过CAS尝试获得锁,未获到再进入队列排队。执行速度快,吞吐量大但容易导致线程饿死。
死锁
条件:
- 互斥条件
- 循环等待
- 不可剥夺
- 持有时等待
线程池
复用线程,避免频繁创建与销毁线程的开销。
流程
提交任务 → 核心线程是否已满?
├─ 未满 → 创建核心线程执行
└─ 已满 → 任务入队
├─ 队列未满 → 等待执行
└─ 队列已满 → 创建非核心线程
├─ 未达最大线程数 → 执行任务
└─ 已达最大线程数 → 执行拒绝策略
参数
- corePoolSize:核心线程数,当当前线程数<=核心线程数,则即使空闲也不会销毁
- maximumPoolSize:最大线程数,线程池能创建的最大数量,等待队列满了之后,就会着手创造新的非核心线程(空闲后销毁),当当前线程数<=最大线程数时,触发拒绝策略。
- keepAliveTime:非核心线程的存活时间,超过之后销毁。
- unit:keepAliveTime的单位
- threadFactory:线程工厂,给线程取名字。
- handler:拒绝策略:
- CallerRunPolicy:让线程池调用者所在的线程去执行被拒绝的任务。
- AbortPolicy:抛出异常
- DiscardPolicy:不做处理,静默拒绝
- DiscardOldestPolicy:将最老的线程任务拒绝,执行当前任务。
线程池种类
ScheduledThreadPool:支持定时或周期性执行任务。FixedThreadPool:核心线程数和最大线程数是一样的,CachedThreadPool:可缓存线程池,线程数是几乎可以无限增加的(实际最大可以达到 Integer.MAX_VALUE),当线程闲置时可以对线程进行回收。有一个用于存储提交任务的队列,但这个队列是 SynchronousQueue,队列的容量为0,实际不存储任何任务,它只负责对任务进行中转和传递,所以效率比较高。SingleThreadExecutor:使用唯一的线程去执行任务,如果线程在执行任务的过程中发生异常,线程池也会重新创建一个线程来执行后续的任务。适合用于所有任务都需要按被提交的顺序依次执行的场景。SingleThreadScheduledExecutor:是ScheduledThreadPool的一个特例,内部只有一个线程。
shutdown()
shutdown():将池子状态置为SHUTDOWN,正在执行的任务继续执行,等待执行的任务直接中断。
shutdownNow():状态置为STOP,正在执行的任务尝试停止。