文章目录
需要掌握的内容
1.线程的一般创建方法(三种方式),Thread两种创建方式的源码
2.线程的重要api
3.线程的状态
4.线程应用:异步调用,同步等待,并行计算,统筹规划
5.线程的原理:运行流程,重要概念
6.两阶段终止的写法和作用
1. 进程与线程
1.1 进程和线程都是个啥
1.资源分配的角度:进程是系统资源分配的最小单位,而引入线程后线程是是资源调度的最小单位
2.从包含关系角度:进程是线程的容器,一个进程可以分为多个线程
3.从原理角度:进程可以看成外存中程序在内存中的实例,程序被运行,就开启了一个进程,可以加载指令,管理内存和IO。而线程是一个指令流,将一条条指令按照一定顺序交给cpu来执行。
4.二者关系:进程所被分配的资源由它内部的线程共享,进程间通信比较复杂,而线程通信比较简单。上下文切换的开销线程比进程更低。
2.并行与并发
单核CPU将时间分成小片给不同的线程来使用,只是我们看来是同时运行,这种过程称之为为并发Concurrent。
多核CPU,不同的核心可以调度运行线程,那么真正意义上实现了同时运行,称之为并行
3.线程的运行
3.1 线程的创建
3.1.1 直接使用Thread类
// 构造方法的参数是给线程指定名字
Thread t1 = new Thread("t1") {
@Override
// run 方法内实现了要执行的任务
public void run() {
log.debug("hello");
}
};
t1.start();
3.1.2 使用Runnable与Thread
// 创建任务对象
Runnable task2 = new Runnable() {
@Override
public void run() {
log.debug("hello");
}
};
// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();
//lambda表达式
// 创建任务对象
Runnable task2 = () -> log.debug("hello");
// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();
既然可以直接创建,为什么还要实现Runnable接口?这有什么好处吗?
1、分离了线程代码和任务代码,例如在有多个相同任务的不同线程分享同一个资源的情况下更容易实现,减少重复代码
2、实现了Runnable接口,显然还可以继承别的类,编码更加灵活
3.1.3 使用FutureTask与Thread
FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况
// 创建任务对象
FutureTask<Integer> task3 = new FutureTask<>(() -> {
log.debug("hello");
return 100;
});
// 参数1 是任务对象; 参数2 是线程名字,推荐
new Thread(task3, "t3").start();
// 主线程阻塞,同步等待 task 执行完毕的结果
Integer result = task3.get();
log.debug("结果是:{}", result);
4. Linux查看线程常用命令(掌握)
查看当前运行所有进程,当前时刻
ps -fe
查看从属于某个进程号的所有线程
ps -fT -p <PID>
ps -ef |grep - 列出需要进程
ps -aux - 显示进程信息,包括无终端的(x)和针对用户(u)的进程:如USER, PID, %CPU, %MEM等
杀进程与强制杀进程
kill <PID>
kill -9 <PID>
查看某个进程的所有线程,占用从高到低排列,可以持续监视,看看如CPU,资源使用的情况
top -H -p <PID>
5. 线程运行的原理
线程交替运行,先后不受我们控制
5.1 线程栈帧(结合JVM)
JVM占内存是给线程用的,每一个线程有自己对应的一个栈空间,一个栈有一组栈帧,每个栈帧对应一次方法的调用,每个线程只能有一个活动栈帧,方法执行完,栈帧就被释放掉。
5.2线程运行的流程
1.类加载:将字节码放到方法区内存中
2.启动main主线程的线程栈和程序计数器,为main方法分配栈帧内存,在栈帧分配时就会把局部变量表,返回地址等等准备好
3.
5.3线程上下文切换
是指cpu不再执行当前的线程从而转向另一个线程的情况
上下文切换的情况:
1.CPU时间片用完
2.垃圾回收gc
3.有更高级的线程需要进行
4.线程自己调用了一些方法来自我让出,包括sleep,yield,wait,join,park,synchronized,lock等方法
上下文切换发生是要保存当前线程的状态,并且要恢复另一个线程的状态
程序计数器:记住下一条jvm指令的执行地址
状态包括:
上下文频繁切换会影响性能
6. 线程中的常见方法
start()准备运行就绪,具体什么时候开始是根据OS来分配
run()新线程启动就会使用该方法
join() 等待线程运行结束,在两个线程通信时使用
join(long n)
getId()
setName(String)
getPriority()
setPriority()
getState()
IsInterrupted()判断是否被打断
interrupted
IsAlive()判断线程是否存活,true代表还在运行
currentThread()返回当前运行的对象
sleep(long n)休眠n毫秒
yield()主要是为了调试
6.1 start()与run()
在这里插入代码片
如果直接用线程对象实例t1来调用run(),那么run()方法实际上是被main线程所调用执行,没有启动新的线程,如果主线程接下来还有一些耗费时间的操作,则不能达到异步等效果,所以必须要使用start()方法来启动。
start()前后线程状态对比
前:NEW 后:RUNNABLE可被调用,此时不能再调用start(),不然会报线程状态异常
6.2 sleep与yield
6.2.1 sleep()
1.让进程从RUNNING到TIMED_WAITING(阻塞状态)
2.其他线程可以用interrupted方法打断TIMED_WAITING的线程,sleep会跑InterruptedException异常
- 睡眠结束后的线程变成RUNNABLE状态不一定会马上执行
4.TimeUnit的sleep比Thread的sleep()可读性好
6.2.2 yield
1.让线程从Running到Runnable
2.具体实现依赖于OS的任务调度器
6.2.3 yieldVSsleep
1.sleep后任务调度器不会调度执行该线程,而yield后存在被调度执行的可能
2.sleep有持续时间,而yield没有
6.2.4 线程优先级(不靠谱)
1.是个提示hint,但是实际上一般情况下没有yield力度大
2.CPU闲时没啥用,会被忽略掉
3.本质还是因为线程调度是由OS的任务调度器来决定的
CASE:如何防止CPU占用过高
场景:服务器需要运行一个死循环来不断获取请求,导致cpu占用过高
解决:在没有真正使用cpu使,不要使循环空转,那么就可以用sleep或者yield来让出cpu的使用权
1.利用sleep()实现
对于无锁不需同步的场景,用sleep就挺好
注: 可以通过wait或者条件变量来达到类似效果,但是这两种实现方式都需要加锁以及唤醒操作,更适用于需要同步的场景
2.利用yield()实现
6.3 join()方法
CASE:主线程打印与线程1中的赋值
6.3.1 join是干嘛的
join是用来等待指定的某个线程运行结束的
threadName.join();
CASE:线程同步案例1 主线程等待多个线程
其中线程t1睡眠1s,t2睡眠2s。左图为t1先启动,t2随之启动的情况。右图为t2先启动,t1随之启动的情况
左图:t1启动了以后睡1s,同时启动的t2睡2s,t1醒了以后,CPU空闲,马上执行r=10,然后进入等待,又过了1s,t2醒了,发现CPU也是空闲的,马上执行r=20,然后进入等待结束。t1.join()到t2.join()只要等1s
右图:t2.join()到t1.join根本不用等
6.3.2 有时效的join(long n)
n限制了等待的最长时间,如果时间超过n毫秒,则会继续执行join后的代码,如果线程提前结束了,也会停止等待
6.4 interrupt()方法
6.4.1 用法:打断阻塞状态的线程
打断标记:boolean型,在阻塞状态以后被打断以后会抛出异常,标记被置为false,而正常运行时标记置为true
对于正常运行的线程,可以根据打断标记来控制是否打断,如上所示,当打断标记为true,则退出循环。可以用打断来停止线程,由正在运行的线程自己决定。
6.4.2 两阶段终止模式
可以理解为线程T1对T2的赐死模式
不能使用的方法:
1.stop()方法被ban了,因为对于对共享资源加锁的线程,被stop方法干掉以后就没办法释放锁了
2.System.exit()也被ban了,因为整个程序进程都没了。
3.suspend()和resume()也被ban了,用wait()不香吗?
那我们该怎样让T2体面呢?那就要考虑两种被打断的情况。
细节:
1.在sleep()时被打断会出现异常 ,要捕异常然后重置打断标记为true
2.不要用错成了interrupted(),这个静态方法会直接把打断标记给清理成true
6.5 park()方法
LockSupport.park()方法会使该语句后面的语句无法运行,但是打断标记为真的情况下,LockSupport.park()就会失效。可用interrupted()方法来实现打断标记的重置。
7. 守护线程
只要有一个线程运行,java进程就不会结束。守护线程是这样的:只要其他的费守护线程运行结束了,即使守护线程的代码都没有执行完,也会强制性结束。
设置守护线程:
threadName.setDaemon(true);
守护线程的用处:
1.垃圾回收器线程
2.Tomcat中的Acceptor与Poller线程
8. 线程状态
8.1 从操作系统的角度:5种状态
初始状态:创建了线程对象,还未与操作系统的线程相关联
可运行状态:RUNNABLE 此时线程已经和操作系统的线程关联,可以由CPU调度执行
运行状态:get了CPU时间片,运行中的状态
阻塞状态:当调用了阻塞API时,会导致线程的上下文切换,进入阻塞状态。分时间片的时候不会分给阻塞状态的线程。
终止状态:线程生命周期的结束
8.2 从javaAPI的层面:六种状态
javaThread类里面的枚举State,将线程状态分为六种状态
Java中运行状态和正在运行都是RUNNABLE,涵盖了可运行,运行状态,和线程阻塞(BIO,读文件)
Java对阻塞状态还做了细分,BLOCKED(资源被加了锁,拿不到资源所陷入的状态),WAITING,TIMED_WAITING(有时限的等待)
9. 课后习题
用两个线程实现上图,即对两个人烧水泡茶的合理分配,其中洗茶壶1min,洗茶杯min,拿茶叶1min
9.1 分析与代码实现:
该题目训练方法的用法,两个人是两个线程t1与t2,每次动作都可以看作是一次输出,而持续时间可以用sleep()来实现。
package com.cyan.n2.util;
import java.util.concurrent.TimeUnit;
/**
* @author Cyan
* @version 1.0
* @description: 重写了sleep方法,不用每次都写try catch,输入参数简化到秒
* @date 2021/12/1 21:34
*/
public class Sleeper {
//写入参数为整数
public static void sleep(int i){
try{
TimeUnit.SECONDS.sleep(i);
}catch (InterruptedException e){
e.printStackTrace();
}
}
//写入参数为double
public static void sleep(double i){
try{
TimeUnit.MILLISECONDS.sleep((int)i*1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
//真正的程序
package com.cyan.test;
import com.cyan.n2.util.Sleeper;
import lombok.extern.slf4j.Slf4j;
/**
* @author Cyan
* @version 1.0
* @description: 烧水泡茶
* @date 2021/12/1 21:17
*/
@Slf4j(topic = "c.Test16Assignment")
public class Test16Assignment {
public static void main(String[] args) {
//创建新线程
Thread t1 = new Thread(()->{
log.debug("洗水壶");
Sleeper.sleep(1);
log.debug("烧开水");
Sleeper.sleep(15);
},"t1");
Thread t2 = new Thread(()->{
log.debug("洗茶壶");
Sleeper.sleep(1);
log.debug("洗杯子");
Sleeper.sleep(2);
log.debug("拿茶叶");
Sleeper.sleep(1);
//等线程t1烧完水然后才能去泡茶
try{
t1.join();
}catch (InterruptedException e){
e.printStackTrace();
}
log.debug("泡茶");
},"t2");
//启动线程
t1.start();
t2.start();
}
}
运行结果:
可以看到线程t2洗茶壶和t1洗水壶动作同时开始,然后各睡眠1s以后,t1开始烧开水,然后马上进入持续15s的TIMED_WAITING,t1开始烧开水的同时,t2就开始洗杯子,进入2s的TIMED_WAITING然后cpu未被占用,又开始拿茶叶,再进入1s的TIMED_WAITING,然后等待t1至烧水烧开,再进行泡茶
可以看到实际上t1和t2是各忙各的,没有进行通信,这里留下了两个思考
1.t1和t2之间交换了操作怎么搞。。。
2.t2和t1之间如果有一些交换,比如要把洗好的杯子交给另一个线程。。。