1.线程的基础知识
进程:
操作系统分配资源的单元
哪些资源:指令,内存管理,IO管理,磁盘资源,CPU时间片
线程: 进程中的实体
区别:
- 进程基本相互独立,线程存在于进程内,
- 进程拥有共享资源,供内部线程共享
- 进程通信复杂
同一机器的通信
不同机器的通信:网络
进程间的通信方式:
- 管道:用于父子进程间,还有有名管道,允许无亲缘关系进程的通信
- 信号:软件层次对中断机制的模拟,比较复杂,用在通知进程事件发生,与中断请求效果一样
- 消息队列:消息的链接表:克服信号量有限的缺点,
- 共享内存:最有用的方式,依靠同步操作,互斥锁,信号量
- 信号量:进程间或同一进程间不同线程之间的同步互斥手段
- 套接字:用户网络间的不同机器的通信
线程同步互斥
线程间的制约关系,一个线程依赖另一个线程的消息
线程互斥:线程对共享的资源,在访问时的排他性,一种特殊的线程同步
四种线程同步互斥的控制方法
- 临界区 :串行访问的共享资源
- 互斥量:协同对同一资源单独访问
- 信号量:限制有限用户资源
-
上下文切换
CPU:从一个线程或进程切换到另一个线程或进程
内核(即操作系统的核心)对CPU上的进程(包括线程)执行以下活动 暂停一个进程的处理,并将该进程的CPU状态(即上下文)存储在内存中的某个地方
- 从内存中获取下一个进程的上下文,并在CPU的寄存器中恢复它
- 返回到程序计数器指示的位置(即返回到进程被中断的代码行)以恢复进程。
上下文切换通常是计算密集型的
尽可能地避免不必要的上下文切换
操作系统层面的线程什么周期
五种: 初始状态、可运行状态、运行状态、休眠状态和终止状态
- 初始状态:已创建,为分配CPU,语音意义上的创建,而非操作系统层面
- 可运行状态:线程可以分配CPU执行,操作系统层面的线程已被创建,可以分配CPU了
- 运行状态:被分配的CPU 线程
- 休眠状态:调用阻塞的api,等待,就会使休眠状态,释放CPU使用权,休眠的线程永远没有机会获得CPU使用权,需要等待事件通知线程进入可运行状态
- 终止状态:执行完成或异常,终止状态的线程不会切换到其他状态,等待销毁?
Java中吧可运行状态与运行状态合并了
但在操作系统调度有用,JVM不关注,
2:java线程详解
2.1 Java线程的实现方式
方式一:使用 Thread类或继承Thread类
// 创建线程对象
Thread t = new Thread() {
public void run() {
// 要执行的任务
}
};
// 启动线程
方式二:实现 Runnable 接口配合Thread
把【线程】和【任务】(要执行的代码)分开
- Thread 代表线程
- Runnable 可运行的任务(线程要执行的代码)
方式三:使用有返回值的 CallableRunnable runnable = new Runnable() {
public void run(){
// 要执行的任务
}
};
// 创建线程对象
Thread t = new Thread( runnable );
// 启动线程
方式四:使用 lambdaclass CallableTask implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return new Random().nextInt();
}
}
//创建线程池
ExecutorService service = Executors.newFixedThreadPool(10);
//提交任务,并用 Future提交返回结果
总结其实都是 Thread#startnew Thread(() -> System.out.println(Thread.currentThread().getName())).start();
Thread.start 与run的区别
2.2 Java线程的实现原理
Java线程属于内核级线程
涉及Thread#start()的源码了
Thead#start方法详解
https://www.processon.com/view/link/5f02ed9e6376891e81fec8d5
总结:
基于操作系统原生线程模式来实现,一条Java线程映射一条轻量级线程
内核线程:内核态
用户级线程:操作系统不知道他的存在
协程
不归操作系统内核管理,由用户程序控制,内核不可见
协程优势:
- 不涉及内核,减少上下文切换,效率高
- 协程大小1K,数量更多
- 不需要多线程锁,只有一个线程,
注意: 协程适用于被阻塞的,且需要大量并发的场景(网络io)。不适合大量计算的场景
2.3 线程的调度机制
一:协同式线程调度
线程时间由线程本身控制,线程完成后,会通知系统切换到另一个线程
好处:
实现简单:,切换对线程可知,
坏处:
线程执行时间不可控,万一有问题会一直阻塞
二:抢占式线程调度
每个线程由系统分配,线程的切换不由线程本身来决定,
执行时间可控,也不会造成一个线程堵塞
Java中,Thread.yield()可以让出执行时间,但无法获取执行时间
优先级并不是很靠谱,因为Java线程是通过映射到系统的原生线程上来实现的,所以线程调度最终还是取决于操作系统
2.4 Java线程的生命周期
Java中线程六种状态,分别是:
- new :初始化
- runnable:可运行 + 运行状态
- BlockEd : 阻塞
- waiting : 无时限的等待
- TIMED_WAITING :有时限的等待
- TERMINATED:终止
操作系统层面: BOLKED,WATING, timed_waiting是一种状态, 休眠
都是永远没有CPU的使用权,需要事件通知他
JavaThread角度JVM定义的状态
操作系统角度定义的
2 .5 Thread的常用方法
sleep :
- 会从running 进入 timed_waiting ,不会释放锁
- 其他线程可以使用interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException,并且会清除中断标志
- 睡眠结束后的线程未必会立刻得到执行
-
yield:
会释放CPU ,从running进入runnable,但不会释放锁对象
- main方法yield 不会让出去,它优先级最高
-
join:
等待调用join方法的线程结束之后,程序再继续执行,一般用于等待异步线程执行完结果之后才能继续运行的场景
public class ThreadJoinDemo {
public static void main(String[] sure) throws InterruptedException {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("t begin");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t finished");
}
});
long start = System.currentTimeMillis();
t.start();
//主线程等待线程t执行完成
t.join();
System.out.println("执行时间:" + (System.currentTimeMillis() - start));
System.out.println("Main finished");
}
stop
interrupt
Java线程的中断机制
- interrupt(): 将线程的中断标志位设置为true,不会停止线程
- isInterrupted(): 判断当前线程的中断标志位是否为true,不会清除中断标志位
- Thread.interrupted():判断当前线程的中断标志位是否为true,并清除中断标志位,重置为fasle
利用中断机制优雅的停止线程public class ThreadInterruptTest {
static int i = 0;
public static void main(String[] args) {
System.out.println("begin");
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
i++;
System.out.println(i);
//Thread.interrupted() 清除中断标志位
//Thread.currentThread().isInterrupted() 不会清除中断标志位
if (Thread.currentThread().isInterrupted() ) {
System.out.println("=========");
}
if(i==10){
break;
}
}
}
});
t1.start();
//不会停止线程t1,只会设置一个中断标志位 flag=true
t1.interrupt();
}
使用中断机制时一定要注意是否存在中断标志位被清除的情况public class StopThread implements Runnable {
@Override
public void run() {
int count = 0;
while (!Thread.currentThread().isInterrupted() && count < 1000) {
System.out.println("count = " + count++);
}
System.out.println("线程停止: stop thread");
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new StopThread());
thread.start();
Thread.sleep(5);
thread.interrupt();
}
sleep可以被中断 抛出中断异常:sleep interrupted, 清除中断标志位
wait可以被中断 抛出中断异常:InterruptedException, 清除中断标志位
2. 6 java 线程间通信
volatile
volatile有两大特性,一是可见性,二是有序性,禁止指令重排序,其中可见性就是可以让线程之间进行通信。
等待唤醒(等待通知)机制
1:wait notify
调用该线程锁对象的wait方法,线程将进入等待队列进行等待直到被唤醒。
public class WaitDemo {
private static Object lock = new Object();
private static boolean flag = true;
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock){
while (flag){
try {
System.out.println("wait start .......");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("wait end ....... ");
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
if (flag){
synchronized (lock){
if (flag){
lock.notify();
System.out.println("notify .......");
flag = false;
}
}
}
}
}).start();
}
2: park &unpark
LockSupport:
jdk用来实现线程阻塞,和唤醒的工具,调用park等待许可,UNpark为指定线程提供许可,
public class LockSupportTest {
public static void main(String[] args) {
Thread parkThread = new Thread(new ParkThread());
parkThread.start();
System.out.println("唤醒parkThread");
LockSupport.unpark(parkThread);
}
static class ParkThread implements Runnable{
@Override
public void run() {
System.out.println("ParkThread开始执行");
LockSupport.park();
System.out.println("ParkThread执行完成");
}
}
管道输入输出流(了解)
管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于,它主要用于线程之间的数据传输,而传输的媒介为内存。管道输入/输出流主要包括了如下4种具体实现:PipedOutputStream、PipedInputStream、PipedReader和PipedWriter,前两种面向字节,而后两种面向字符。
public class Piped {
public static void main(String[] args) throws Exception {
PipedWriter out = new PipedWriter();
PipedReader in = new PipedReader();
// 将输出流和输入流进行连接,否则在使用时会抛出IOException
out.connect(in);
Thread printThread = new Thread(new Print(in), "PrintThread");
printThread.start();
int receive = 0;
try {
while ((receive = System.in.read()) != -1) {
out.write(receive);
}
} finally {
out.close();
}
}
static class Print implements Runnable {
private PipedReader in;
public Print(PipedReader in) {
this.in = in;
}
@Override
public void run() {
int receive = 0;
try {
while ((receive = in.read()) != -1) {
System.out.print((char) receive);
}
} catch (IOException ex) {
}
}
}
Thread.join
理解为线程合并,一个线程调用另一个线程的join,当前会阻塞,等待被调用的join线程完成才会继续
可以保证顺序,就变成了串行,而非并行
join的实现其实是基于等待通知机制的
3:扩展
3.1内核模式 用户模式
https://note.youdao.com/ynoteshare/index.html?id=a12216347600326ac714d4539db03585&type=note&_time=1647267913299
线程之所以重,涉及到了内核
协程轻,仅在用户层
3.2 一些命令
linux系统可以通过命令统计CPU上下文切换数据
vmstat 1 #可以看到整个操作系统每1秒CPU上下文切换的统计
cs列就是CPU上下文切换的统计
造成CPU上下文切换的操作:
- 线程,经常切换
- 系统调用
- 中断
pidstat ‐w ‐p 5598 1 # 显示进程5598每一秒的切换情况
常用的参数:
-u 默认参数,显示各个进程的 CPU 统计信息
-r 显示各个进程的内存使用情况
-d 显示各个进程的 IO 使用
-w 显示各个进程的上下文切换
-p PID 指定 PID
cswch表示主动切换
- 从进程的状态信息中查看
cat /proc/5598/status 查看进程的状态信息
voluntary_ctxt_switches: 40469351
nonvoluntary_ctxt_switches: 2268
查看进程线程的方法
windows
- 任务管理器可以查看进程和线程数,也可以用来杀死进程
- tasklist 查看进程
- taskkill 杀死进程
linux
- ps -fe 查看所有进程
- ps -fT -p 查看某个进程(PID)的所有线程
- kill 杀死进程
- top 按大写 H 切换是否显示线程
- top -H -p 查看某个进程(PID)的所有线程、
Java
- jps 命令查看所有 Java 进程
- jstack 查看某个 Java 进程(PID)的所有线程状态
- jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)
Linux系统中线程实现方式
- LinuxThreads linux/glibc包在2.3.2之前只实现了LinuxThreads
- NPTL(Native POSIX Thread Library)
getconf GNU_LIBPTHREAD_VERSION #查看系统是使用哪种线程实现
问题:
1.CAS涉及到用户模式到内核模式的切换吗?
2.为什么说创建Java线程的方式本质上只有一种?Java线程和go语言的协程有什么区别?
3.如何优雅的终止线程?
4.Java线程之间如何通信的,有哪些方式?