多线程
# 1 基本概念与术语
# 1.1 进程与线程
进程是操作系统分配资源的基本单位。线程是处理器任务调度和执行的基本单位。一个进程包含一个或多个线程。
进程是相互独立的,每个进程都有独立的代码和数据空间(程序上下文),进程的切换会有较大的开销。同一进程的线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换开销小。
一个进程崩溃后,一般不会影响到其他进程。一个线程崩溃可能会导致整个进程死掉。
举例:WPS是一个进程,我们在打字的同时,WPS可以进行拼写检查,打字跟拼写检查可以理解为两个线程。
# 1.2 并行与并发
串行:在同一时刻,有一个任务在单个CPU上依次执行。
并行:在同一时刻,有多个任务在多个CPU上同时执行。
并发:在同一时刻,有多个任务在单个CPU上交替执行。
# 1.3 线程调度
# 1.3.1 线程并发执行
计算机中的CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU的使用权才能执行代码。各个线程轮流获得CPU的使用权,分别执行各自的任务。
# 1.3.2 线程调度模型
分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU时间片。 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选取一个,优先级高的线程获取CPU的时间片相对多一些。
# 1.4 可重入锁
JVM允许同一个线程重复获取同一个锁,被同一个线程反复获取的锁,叫做可重入锁。
# 1.5 死锁
线程死锁,是由于两个或更多的线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法继续执行。
死锁是因为锁的嵌套产生的,所以避免死锁的根本就是要避免锁的嵌套。多线程获取锁的顺序要一致。
# 2 创建线程的方式
# 2.1 继承Thread类
继承Thread类,并重写run方法。
# 2.2 实现Runnable接口
实现Runnable接口,并重写run方法。
# 2.3 实现Callable接口
实现Callable接口,并重写call方法。
call方法有返回值的,可以抛出异常。Callable接口支持泛型。实现Callable接口的派生类需要结合FutureTask一起使用。
JDK5新增的。
- Future可以对Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
- FutureTask是Future接口唯一的实现类
- FutureTask同时实现了Runnable、Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。
# 2.4 线程池
提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁,实现重复利用。
JDK5新增的。
ExecutorService接口 + Executors工具类
// 创建固定大小的线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
// 提交任务
executorService.execute(task1);
executorService.submit(task2);
2
3
4
5
6
# 3 线程的生命周期
1、当new Thread时,线程处于新建状态;
2、当调用start方法时或sleep方法时间到/阻塞结束,线程处于就绪状态;
3、当线程抢到CPU的执行权时,线程处于运行状态;
4、当线程执行到sleep方法或其他阻塞方法时,线程处于阻塞状态;
5、当run方法结束时,线程处于死亡状态。
# 4 线程的安全问题
解决线程之间共享数据的安全问题有以下3种方式。
# 4.1 使用volatile
volatile关键字解决的是可见性问题:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值。
public volatile boolean myVar = true;
# 4.2 使用synchronized
同步代码块
synchronized(lock){
// 业务逻辑
}
2
3
同步方法
public synchronized void myMethod(){
// 业务逻辑
}
2
3
# 4.3 使用ReentrantLock
Lock,JDK5新增的
private final Lock lock = new ReentrantLock();
try{
lock.lock();
// 业务逻辑
}
finally{
lock.unlock();
}
2
3
4
5
6
7
8
9
# 4.4 几种方式比较
volatile关键字解决的是可见性问题:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值。
synchronized是独占锁,加锁和解锁的过程自动进行,易于操作,但不够灵活。
ReentrantLock也是独占锁,加锁和解锁的过程需要手动进行,不易操作,但非常灵活。
# 5 线程通信的应用
生产者消费者模式,也叫等待唤醒机制,是一个非常经典的多线程协作模式。
一个线程负责生产数据,放到共享区域,然后通知另一个线程去消耗数据。
synchronized + wait + notify 实现多线程协调
public synchronized void send() throws InterruptedException { if(条件){ // 业务逻辑 notify(); } else { wait(); } }
1
2
3
4
5
6
7
8
9Lock + Condition 实现多线程协调
private final Lock lock = new ReentrantLock(); private final Condition condition = lock.newCondition(); public void send() throws InterruptedException { try{ lock.lock(); if(条件){ // 业务逻辑 condition.signal(); // 相当notify } else { condition.await(); // 相当于wait } } finally{ lock.unlock(); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Condition提供的await()、signal()、signalAll()原理和synchronized锁对象的wait()、notify()、notifyAll()是一致的,并且其行为也是一样的
# 6 扩展知识
# sleep 和 wait 的区别
相同点:
- 调用sleep或wait,线程都会进入阻塞状态。
不同点:
- 声明位置不同:Thread类中声明sleep,Object类中声明wait;
- 调用位置不同:sleep在任何地方都可以使用,wait只能用在同步代码块或者同步方法中;
- 关于是否释放同步监听器:如果两个方法都用在同步代码块或者同步方法中,sleep不会释放,wait会释放。
# wait、notify、notifyAll
三个方法都是定义在Object类中;
三个方法只能使用在同步代码块或同步方法中;
三个方法的调用者必须是同步代码块或同步方法中的同步监听器;
一旦调用wait方法,线程就会进入阻塞状态,释放同步监听器;
一旦调用notify方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级最高的那个;
一旦调用notifyAll方法,就会唤醒所有被wait的线程。
# 参考资料
多线程 - 廖雪峰的官网网站 (opens new window)
Java -Java 学习- Java 基础到高级-宋红康-零基础自学Java-尚硅谷 (opens new window)
Java中Atomic类的使用分析 (opens new window)
锁机制
锁升级
偏向锁、轻量级锁、重量级锁
AQS是什么?AQS如何实现可重入锁?
可重入锁
可重入锁,也称递归锁。
可以重复获取相同的锁。
https://blog.csdn.net/w8y56f/article/details/89554060
如果一个线程在执行一个持有锁的方法,在这个方法中调用另一个持有相同锁的方法,则该线程可以直接调用,而无需重新获取锁。
2
3
4
5
6