并发编程
1.1并发编程发展
略
2.进程与线程
2.1进程与线程
进程
程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至cpu,数据加载至内存。在指令运行过程中还需要用到磁盘,网络等设备,进程就是用来加载指令、管理内存、管理IO的
一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程
进程可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(记事本,浏览器等),也有的程序只能启动一个实例进程(360安全卫士等)
线程
一个程序之内可分为一到多个线程。
一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行
java中,线程作为最小调度单位,进程作为资源分配的最小单位,windows中进程是不活动的,只是作为线程的容器
二者对比
进程基本是相互独立,而线程存在于进程内,是进程的一个子集
进程拥有共享的资源,如内存空间等,供其内部的线程共享
进程间通信较为复杂
同一台计算机的进程通信称为IPC
不同计算机的进程通信,需要通过网络。并遵守共同的协议,比如http
线程通信共享进程内的内存,比如多个线程可以访问同一个共享变量
线程更轻,线程上下文切换成本一般要比进程上下文切换低
2.2并行与并发
并发:单核cup,线程实际是串行执行,操作系统的任务调度器将cpu时间片分给不同的线程使用,只是cpu在线程间切换很快,感觉就像同时运行。总结:微观串行,宏观并行。
线程轮流使用cpu的做法称为并发。concurrent
并行:多核cpu下,每个核都可以调度运行线程,这时候线程可以是并行的。
概括:
并发:是同一时间应对多件事情的能力
并行:是同一时间动手做多件事情的能力
2.3应用之异步调用
同步:需要等待结果返回,才能继续运行(在多线程中,指让多个线程步调一致)
异步:不需要等待结果返回,就能继续运行
应用:tomcat的异步servlet,让用户线程处理耗时较长的操作,避免tomcat的工作线程
结论:
1.单核cpu下,多线程不能实际提高程序的运行效率,多线程反而更慢。但是可以在不同的任务之间切换,不同线程轮流使用cpu,不至于一个线程总占用cpu,别的线程没法干活
2.多核cpu可以并行跑多个线程,但能否提高程序效率得分情况(有些可以拆分并行执行,但不是所有都能拆分,【阿姆达尔定律】)
3.IO操作不占cpu,我们一般拷贝文件使用的是【阻塞io】,这时相当于线程虽然不用cpu,但是需要一直等待io结束。后面有【非阻塞io】和【异步io优化】
3.java线程
3.1创建和运行线程
方法一,直接使用Thread
//创建线程对象,t1为线程名字
Thread t1 = new Thread("t1"){
@Override
public void run(){
//要执行的任务
System.out.println("hello");
}
};
t1.start();
方法二,使用Runnable配合Thread
Thread代表线程
Runnable可运行的任务(线程要执行的代码)
Runnable runnable = new Runnable(){
@Override
public void run() {
System.out.println("helllo");
}
};
Thread t = new Thread(runnable,"T2");
t.start();
// lambda表达式写法
Runnable runnable = ()-> { System.out.println("helllo"); };
Thread t = new Thread(runnable,"T3");
t.start();
方法三,FutureTask配合Thread
FutureTask能够接收Callable类型的参数,用来处理有返回结果的情况
FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.debug("running...");
Thread.sleep(2000);
return 100;
}
});
Thread t4 = new Thread(task, "t4");
t4.start();
log.debug("t4线程返回的结果:{}",task.get());
3.2查看进程线程的方法
windows
任务管理器可以查看进程和线程,也可以杀死进程
tasklist查看进程
taskkill杀死进程
linux
ps -fe查看所有进程
ps-fT-p 查看某个进程pid的所有线程
kill杀死进程
top按大写H切换是否显示线程
top- H-p 查看某个进程pid的所有线程
Java
jps命令查看所有java进程
jstask 查看某个java进程pid的所有线程状态
jconsole来查看某个Java进程中线程的运行情况
3.3线程运行原理
栈与栈帧
Java虚拟机栈
JVM中由堆、栈、方法区所组成,其中栈内存是给线程用的,每个线程启动后,虚拟机就会为其分配一块栈内存。
每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
线程上下文切换(Thread Context Switch)
因为以下一些原因导致cpu不在执行当前的线程,转而执行另一个线程的代码
线程的cpu时间片用完
垃圾回收
有更高优先级的线程需要运行
线程自己调用了sleep、yield、wait、join、park、synchronized、lock等方法
当Context Switch发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,java中对应的概念就是程序计数器,它的作用是记住下一条jvm指令的执行地址,是线程私有的
状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
Context Switch 频繁发生会影响性能
3.4常见方法
方法名 | static | 功能说明 | 注意 |
start() | 启动一个新线程。在新的线程运run方法中的代码 | start方法只是让线程进入就绪,里面代码不一定立刻运行(cpu的时间片还没分它)。每个线程对象的start方法只能调用一次,如果调用多次就会出现illegalThreadStateException | |
run() | 新线程启动后会调用的方法 | 如果在构造Thread对象时传递了Runnable参数,则线程启动后会调用Runnable中的run方法,否则默认不执行任何操作。但可以创建Thread的子类对象,来覆盖默认行为 | |
join() | 等待线程运行结束 | ||
join(long n) | 等待线程运行结束,最多等待n毫秒 | ||
getId() | 获取线程长整型的id | id唯一 | |
getName() | 获取线程名 | ||
setName() | 修改线程名 | ||
getPriority() | 获取线程优先级 | ||
setPriority(int) | 修改线程优先级 | java中规定线程优先级时1-10的整数,较大的优先级能提高该线程被cpu调度的几率 | |
getSate() | 获取线程状态 | java中线程状态是用6个enum(枚举)表示,分别为:NEW、RUNNABLE、BLOCKED、WAITING、TIMED | |
isInterrupted() | 判断线程是否被打断 | 不会清除 打断标记 | |
isAlive() | 线程是否存活(还没有运行完毕) | ||
interrupt() | 打断线程 | 如果被打断线程正在sleep,wait,join会导致被打断的线程抛出InterruptedException,并清除 打断标记;如果打断的正在运行的线程,则会设置打断标记;park的线程被打断,也会设置打断标记 | |
interrupted() | static | 判断当前线程是否被打断 | 会清除打断标记,设为false |
currentThread() | static | 获取当前正在执行的线程 | |
sleep(long n) | static | 让当前执行的线程休眠n毫秒,休眠时让出cpu的时间片给其他线程 | |
yield() | static | 提示线程调度器让出c当前线程对cpu的使用 | 主要是为了测试和调试 |
3.5start与run的区别
start方法才能开启线程,run方法并不能开启线程,run方法仍然是由主线程运行。
3.6sleep与yield
sleep
1.调用sleep会让当前线程从Running进入 Timed Waiting状态(阻塞)
2.其他线程可以使用interrupt方法打断正在睡眠的线程,这时sleep方法会抛出InterruptedException
3.睡眠结束后的线程未必会立刻得到执行
4.建议使用TimeUnit的sleep代替Thread的sleep来获得更好的可读性
yield
1.调用yield会让当前线程从Running进入Runnable就绪状态,然后调度执行其他线程
2.具体的实现依赖于操作系统的任务调度器
线程优先级
线程优先级会提示(hint)调度器优先调度该线程,但它仅仅只是一个提示,调度器可以忽略它
如果cpu比较忙,那么优先级高的线程会获得更多的时间片,但cpu闲时,优先级几乎没作用
应用至之效率
sleep实现
在没有利用cpu来计算时,不要让while(true)空转浪费cpu,这时可以使用yield或cpu来让出cpu的使用权给其他程序
(单核cpu情况,cpu会被此程序占满,其他程序无法运行)
while(true){
try{
Thread.sleep(50);
}catch (InterruptedException e){
e.printStackTrace();
}
}
可以用wait或条件变量达到类似的效果
不同的是,后两种都需要加锁,并且需要相应的唤醒操作,一般适用于要进行同步的场景
sleep适用于无需锁同步的场景
3.7join方法详解
为什么需要join
static int r=0;
public static void main(String[] args) throws InterruptedException{
test();
}
private static void test() throws InterruptedException{
log.debug("开始");
Thread t1 = new Thread(()-> {
log.debug("开始");
sleep(1);
log.debug("结束");
r=10;
});
t1.start();
log.debug("结果为:{}",r);
}
r结果为 0
用join,加在t1.start()之后即可
join(long n),如果线程结束只需要2秒,n是3秒,那么join方法按照2秒
3.8 interrupt方法详解
打断sleep、wait、join阻塞类的线程
打断sleep的线程,会清空打断状态
private static void test() throws InterruptedException{
Thread t1 = new Thread(()-> {
sleep(1);
},"t1");
t1.start();
sleep(0.5);
t1.interrupt();
log.debug("打断状态:{}",t1.isInterrupted()); //false
}
打断正常运行的线程
不会清空打断状态 interrupted为true
public static void main(String[] args) throws InterruptedException {
Thread t1= new Thread(()->{
while(true){
boolean interrupted = Thread.currentThread().isInterrupted();
if (interrupted){
log.info("被打断了,退出循环");
break;
}
log.info("没被打断了,在运行");
}
},"t1");
t1.start();
Thread.sleep(3);
log.info("interrupt");
t1.interrupt();
}
}
//运行结果
//2023-01-18 14:05:38.370 [main] INFO com.kevin.user.threadDemo.test1 - interrupt
//2023-01-18 14:05:38.368 [t1] INFO com.kevin.user.threadDemo.test1 - 没被打断了,在运行
//2023-01-18 14:05:38.370 [t1] INFO com.kevin.user.threadDemo.test1 - 被打断了,退出循环
打断park
当打断标识是true时park失效
private static void test3() throws InterruptedException{
Thread t1 = new Thread(()-> {
LockSupport.park();
log.info("打断状态:{}",Thread.currentThread().isInterrupted())//此时标识为true
// 执行Thread.interrupted(), 会把标识置为false,下面的park方法就可运行
LockSupport.park(); //失效了
},"t1");
t1.start();
sleep(1);
t1.interrupt(); //打断park,打断标识是true
}
3.9 不推荐的方法
容易破坏同步代码块,造成线程死锁
方法名 | static | 功能说明 |
stop() | 停止线程运行 | |
suspend() | 挂起(暂停)线程运行 | |
resume() | 恢复线程运行 |
两阶段终止模式
在一个线程T1中如何优雅终止线程T2?这里的【优雅】指的是给T2一个料理后事的机会
错误思路
使用stop方法停止线程,stop方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其他线程将永远无法获取锁
使用System.exit(int)方法停止线程 ,会让整个线程停止
正确思路
@Slf4j
public class test2 {
public static void main(String[] args) throws InterruptedException {
Termination termination = new Termination();
termination.start();
Thread.sleep(2000);
termination.stop();
}
}
@Slf4j
class Termination{
private Thread monitor;
//启动监控线程
public void start(){
monitor=new Thread(()->{
while (true){
Thread current = Thread.currentThread();
if (current.isInterrupted()){
log.info("料理后事");
break;
}
try {
Thread.sleep(1000);//情况1,此时打断,为阻塞类的线程,会清掉打断标识,interrupted为false
log.info("执行监控记录");//情况1,此时打断,不会清空打断状态 interrupted为true
} catch (InterruptedException e) {
e.printStackTrace();
//重新设置打断标记
current.interrupt();
}
}
});
monitor.start();
}
//停止监控线程
public void stop(){
monitor.interrupt();
}
}
3.10主线程与守护线程
垃圾回收器线程就是一种守护线程
默认情况,java进程需要等待所有的线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
setDaemon(true) 设置守护线程
Thread t1 = new Thread(()-> {
log.info("开始运行")
sleep(2)
log.info("运行END")
},"t1");
//设置该线程为守护线程
t1.setDaemon(true)
t1.start();
sleep(1);
log.info("主线程运行END")
3.11线程的五种状态
操作系统层面描述
初始状态:仅在语言层面创建了线程对象,还未与操作系统线程关联
可运行状态:(就绪状态)指该线程已被创建(与操作系统线程关联),可以由CPU调度执行
运行状态:值获取了cpu时间片运行中的状态,当cpu时间片用完,会从【运行状态】转至为【可运行状态】,会导致线程上下文切换
阻塞状态:
如果调用阻塞API,如BIO读写文件,这时该线程实际不会用到CPU,会导致线程上下文切换,进入阻塞状态
等BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
与【可运行状态】的区别是,对【阻塞状态】的线程来说只有它们一直不唤醒,调度器就一直不会考虑调度他们
终止状态:表示线程已经执行完毕,生命周期已经结束,不会再转换为其他状态
3.12线程的六种状态
根据Thread.State枚举,分为六种状态
NEW :线程刚被创建,但是还没有调用start()方法
RUNNABLE:当调用了start()方法之后,注意,java API层面的RUNNABLE状态涵盖了操作系统层面的【可运行状态】、【运行状态】和【阻塞状态】(由于BIO导致的线程阻塞,在java里无法区分,仍然认为是可运行的)
BLOCKED,WAITING,TIMED_WAITING都是java API 层面对【阻塞状态】的细分,后面再状态转换详述
TERMINATED当线程代码运行结束