Java复习笔记:多线程与并发第一章
最后更新时间:
基本概念回顾
进程和线程的区别
进程和线程的由来
- 串行:早起的计算机只能执行串行任务,并且遇到用户输入的操作时便会阻塞
- 批处理:预先将用户的指令集中成清单,批量串行处理用户指令,仍无法并发执行
- 进程:进程独占内存空间,保存各自运行状态,相互不干扰且可以互相切换,为并发处理任务提供了可能
- 线程:共享进程的内存资源,相互间切换更便捷,支持更细粒度的任务控制,让进程内的子任务得以并发执行
区别
进程和线程都是一个时间段的描述,是CPU工作时间段的描述。进程和线程都是一个时间段的描述,是CPU工作时间段的描述,不过是颗粒大小不同。
所有与进程相关的资源都被记录在PCB(PCB Process Control Block)中。
PCB:
- 描述信息
- 控制信息
- 资源信息
- 程序段
- 数据段
- CPU现场
它是进程实体的一部分,是操作系统中最重要的记录性数据结构。它是进程管理和控制的最重要的数据结构,每一个进程均有一个PCB,在创建进程时,建立PCB,伴随进程运行的全过程,直到进程撤消而撤消。
进程拥有完整的虚拟内存地址空间,而同一进程下的线程则共享该进程拥有的内存空间。
线程的组成:
- 堆栈寄存器
- 程序计数器
- TCB
进程就是包括上下文切换的程序执行时间总和 = CPU加载上下文+CPU执行+CPU保存上下文。
进程的颗粒度太大,每次都要有上下的调入,保存,调出。
假设存在进程A,其实际分成 a,b,c等多个块组合而成。那么这里具体的执行就可能变成:
进程A得到CPU->CPU加载上下文,开始执行程序A的a小段,然后执行A的b小段,然后再执行A的c小段,最后CPU保存A的上下文。
这里的a,b,c就是线程,也就是说线程是共享了进程的上下文环境,的更为细小的CPU时间段。
进程是资源分配的最小单位,线程是CPU调度的最小单位。
Linux用户态和内核态转换
为什么需要转换
内核态的多线程是如何通过轻量级线程来实现的
什么是系统中断
Java中的进程和线程
Java进程和线程的关系
- 运行一个程序会产生一个进程,进程包含至少一个线程
- 每个进程对应一个JVM实例,多个线程共享JVM里的堆
- Java采用单线程编程模型,程序会自动创建主线程
Thread中的start
和run
方法的区别
使用run
方法会继续使用主线程来执行重写的run
方法里面的内容,而使用start
方法则会开一个新线程来执行。
我们看一下start方法源码
Thread.java
1 |
|
可以看到在start
方法里面主要是使用到了一个native的方法start0()
,该方法调用到了外部的非Java的源码。
可以访问OpenJKD来查询
Thread.c
1 |
|
可以看到start0
方法调用到了JVM_StartThread
方法,而该方法引自jvm.h
在jvm.cpp下的JVM_StartThread
方法里有下面这句话用于创建一个线程
jvm.cpp
1 |
|
搜索上面用于创建线程的方法传入的参数thread_entry
1 |
|
可以看到该方法最后会调用JVM虚拟机JavaCalls::call_virtual
,并传入run_method_name
综上所述:
- 调用
start
方法会:Thread#start()->JVM_StartThread->thread_entry->Thread#run()- 在
thread_entry
时创建一个新的子线程并启动去运行Thread#run()里的方法体
- 在
- 调用
run
方法会:Thread#run()- 当做一个普通的方法调用去调用Thread#run()里的方法体
Thread和 Runnable是什么关系
- Thread类实现了Runnable接口,使得run支持多线程
- 因为类的单一继承原则,推荐使用Runnable接口
实现了Runnable接口是没有start方法的,需要把其对象作为参数去创建一个Thread对象再调用start方法启动
如何给run()
方法传参
- 构造函数传参
- 成员变量传参
- 回调函数传参
处理线程的返回值
- 主线程等待法:让主线程循环等待直到子线程返回
- 使用Thread类的
join()
阻塞当前主线程以等待子线程处理完毕1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public class Test implements Runnable {
@Override
void run() {
try {
Thread.currentThread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
value = "data";
}
public static void main(String[] args) throws InterruptedException {
Test test = new Test();
Thread t = new Thread(test);
t.start();
t.join();
System.out.println("value:" + cw.value);
}
} - 通过Callable接口实现:通过FutureTask or 线程池获取
1 |
|
1 |
|
sleep
和wait
的区别
sleep
是Thread类的方法,wait
是Object类中定义的方法sleep
方法可以在任何地方使用wait
方法只能在synchronized
方法或synchronized
快中使用Thread.sleep
只会让出CPU,不会导致锁行为的改变Object.wait
不仅会让出CPU,还会释放已经占有的同步资源
1 |
|
1 |
|
观察输出可发现,在A获得锁之后B开始等待锁,而A开始wait之后B就获得了锁
notify
和notifyAll
的区别
锁池 EntryList:假设线程A已经拥有了某对象的锁,而其它线程B、C想要调用这个对象的某个synchronized
方法(或块)之前必须先获得该对象锁的拥有权,而恰巧该对象的锁目前正被线程A占用,此时B、C线程就会被阻塞,进入一个地方去等待锁的释放,这个地方便是该对象的锁池。
等待池 WaitSet:假设线程A调用了某个对象的wait()
方法,线程A就会释放该对象的锁,同时线程A就进入到了该对象的等待池中,进入到等待池中的线程不会去竞争该对象的锁。
notifyAll
会让所有处于等待池中的线程全部进入锁池去竞争获取锁的机会notify
会随机选取一个处于等待池中的线程进入锁池去竞争获取锁的机会
1 |
|
1 |
|
1 |
|
- 从输出来看前3行是WT1、WT2、WT3依次进入等待池,
- 之后NT1调用
notify
方法随机唤醒一个线程将其置入锁池,并修改go = true;
跳出循环 - 这里是WT1被置入锁池,因为上一步中
go
的值被修改所以跳出循环,WT1获得锁并且修改了变量go = false;
,然后因为NT1线程已经结束所以剩下两个线程WT2、WT3依然处于等待池。
yield
当调用Thread.yield()
时会给线程调度器scheduler
一个当前线程愿意让出CPU的使用的信号,但是调度去可能会无视该暗示。
Thread.java
1 |
|
1 |
|
输出:
1 |
|
可以看出当A线程执行到5时把CPU让给了B来执行,直到B执行到10把B让给A
使用interrupt
来中断线程
已被抛弃的方法:
:过于暴力,被中断线程可能没有释放锁stop()
,suspend()
resume()
interrupt()
- 如果线程处于被阻塞状态,那么线程将立即退出被阻塞状态,并抛出一个
InterruptedException
异常 - 如果线程处于正常状态,那么线程会将该线程的中断标志设置为true。被设置中断标志的线程将继续正常运行,不受影响。
1 |
|
输出:
1 |
|
线程的状态
Thread.java
1 |
|
Java线程状态:
- 新建(NEW):创建后尚未启动的线程状态
- 运行(RUNNABLE):包含Running(正在执行)和Ready(正在等待CPU分配时间片)
- Ready(正在等待CPU分配时间片):其它线程调用了该对象的
start()
方法,该线程位于可运行线程池中,等待被线程调度选中,获取CPU使用权。 - Running(正在执行):就绪状态的线程在获得CPU时间片后变为运行中状态(running)
- Ready(正在等待CPU分配时间片):其它线程调用了该对象的
- 运行(RUNNING):可运行状态(runnable)的线程获得了cpu 时间片(timeslice),执行程序代码
- 无限期等待(WAITING):不会被分配CPU执行时间,需要被显示唤醒,进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)
- 没有设置
Timeout
参数的Object.wait()
方法 - 没有设置
Timeout
参数的Thread.join()
方法 LockSupport.park()
方法
- 没有设置
- 限期等待(TIMED_WAITING):在一定时间后会由系统自动唤醒
Thread.sleep()
方法- 设置了
Timeout
参数的Object.wait()
方法 - 设置了
Timeout
参数的Thread.join()
方法 LockSupport.parkNanos()
方法LockSupport.parkUntil()
方法
- 阻塞(BLOCKED):等待获取排它锁,线程试图获取一个内部对象的
Monitor
(进入synchronized
方法或synchronized
块)但是其他线程已经抢先获取,那此线程被阻塞,知道其他线程释放Monitor
并且线程调度器允许当前线程获取到Monitor
,此线程就恢复到可运行状态。 - 结束(TERMINATED):已终止线程的状态,线程已经结束执行
参考
下图为Oracle支持各个JDK版本所到的年限