并发编程-初级
进程与线程
进程(Process)
- 程序由指令和数据构成。指令需要运行,数据需要读写,就必须将指令加载至CPU,数据加载至内存,在指令运行过程中还需要用到磁盘,网络等设备。
- 进程就是用来加载指令,管理内存,管理IO的。
- 当一个程序被执行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
- 进程可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(如打开多个记事本),但是也有的程序只能启动一个实例进程。(360安全卫士)
线程(Thread)
- 一个进程之内可以分为一至多个线程。
- 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行
- Java中,线程作为最小调度单位,进程作为资源分配的最小单位。Windows中进程是不活动的,只是作为线程的容器。
区别
- 进程基本相互独立,而线程存在进程内,是进程的子集。
- 进程有共享的资源,如内存空间,内部线程可以共享。
- 不同计算机的进程通信需要通过网络,共同遵守网络协议如HTTP。
- 线程通信相对简单,共享进程的内存,多个线程可以访问同一个共享变量。
- 线程更加轻量,其上下文切换成本低于进程。
并行与并发
单核CPU下,线程实际还是串行执行
的,操作系统中有一个组件名为任务调度器,将CPU的时间片(Windows下时间片最小约为15毫秒)分给不同的线程使用,只是由于CPU在线程间(时间片很短)的切换非常快,人类感觉就是同时运行的,即微观串行,宏观并行
线程轮流使用CPU
的做法被成为并发(Concurrent)
CPU | 时间片1 | 时间片2 | 时间片3 | 时间片4 |
---|---|---|---|---|
core | 线程1 | 线程2 | 线程3 | 线程4 |
多核CPU下,每个核(Core)
都可以调度运行线程,称之为并行(parallel)
。
下述两个核心并行,每个核心并发执行线程
CPU | 时间片1 | 时间片2 | 时间片3 | 时间片4 |
---|---|---|---|---|
core1 | 线程1 | 线程2 | 线程3 | 线程4 |
core2 | 线程2 | 线程4 | 线程1 | 线程3 |
同步与异步
从方法调用角度讲
- 需要等待结果返回才能继续执行为
同步
- 不需要等待结果返回就能继续执行为
异步
Java多线程
创建和运行线程的方式
一、直接new Thread()
@Slf4j
public class ThreadExecrise {
public static void main(String[] args) {
//创建线程1
Thread thread1 = new Thread(){
public void run(){
//要执行的任务
log.debug("我是新线程");
}
};
//修改新线程名字
thread1.setName("线程1");
// 启动线程1
thread1.start();
log.debug("我是main线程");
}
}
二、实现Runnable接口创建线程
优点:把线程和任务(执行的代码)分开
Runnable runnable = new Runnable() {
public void run() {
log.debug("我是新线程");
}
};
Thread thread1 = new Thread(runnable,"线程1"); //创建线程
thread1.start();
lambda简化
因为Runnable是函数式接口,即接口中只含有一个抽像方法,因此可以简化。
//lambda简化
Runnable runnable = () -> log.debug("我是新线程");
Thread thread1 = new Thread(runnable,"线程1"); //创建线程
thread1.start();
三、FutureTask接口
接收Callable类型的参数,用来处理有返回值的情况
FutureTask<Integer> task = new FutureTask<Integer>(() -> {
log.debug("我是新线程!");
Thread.sleep(2000);
return 100;
});
new Thread(task,"线程1").start();
//main线程获取返回值,main线程阻塞,同步等待task执行完毕
Integer res = task.get();
log.debug("the result is {}",res); //100
线程常见的方法与区别
run与start
@Slf4j
public class runAndStart {
public static void main(String[] args) {
Thread t1 = new Thread(() -> log.debug("running。。。"));
// t1.run();//并没有开启新线程
t1.start(); //开启了新线程
// t1.start(); //异常,不能多次开启一个线程
}
}
小结
直接调用 run()
是在主线程中执行了 run()
,没有启动新的线程
使用 start()
是启动新的线程,通过新的线程间接执行 run()
方法中的代码
sleep 与 yield
sleep
- 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,那么被打断的线程这时就会抛出
InterruptedException
异常【注意:这里打断的是正在休眠的线程,而不是其它状态的线程】 - 睡眠结束后的线程未必会立刻得到执行(需要分配到cpu时间片)
- 建议用 TimeUnit 的
sleep()
代替 Thread 的sleep()
来获得更好的可读性
yield
- 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
- 具体的实现依赖于操作系统的任务调度器(就是可能没有其它的线程正在执行,虽然调用了yield方法,但是也没有用)
小结
yield使cpu调用其它线程,但是cpu可能会再分配时间片给该线程;而sleep需要等过了休眠时间之后才有可能被分配cpu时间片
线程优先级
线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
-
cpu 比较忙,那么优先级高的线程会获得更多的时间片,
-
cpu 闲时,优先级几乎没作用
join
join再当前线程方法中由其他线程调用,会等待调用方法的线程结束任务后执行
private static void test1() throws InterruptedException {
log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug("开始");
sleep(1);
log.debug("结束");
r = 10;
},"t1");
t1.start();
t1.join(); // 不加join的情况下,多核CPU并行执行,r=0,在主线程main方法中加入t1线程调用join方法后,主线程需等待t1线程任务结束执行
log.debug("结果为:{}", r);// r=10
log.debug("结束");
}
interrupt 方法
打断 sleep,wait,join和正常运行的线程
sleep,wait,join 的线程,这几个方法都会让线程进入阻塞状态, interrupt 方法打断这些线程的阻塞,抛出异常并且清空打断状态,即调用
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("t1线程任务开始sleep...");
try {
Thread.sleep(1000); // wait, join 方法都如此
} catch (InterruptedException e) {
//e.printStackTrace();
log.debug("t1线程被打断了"); //被打断后,返回异常处理结果
}
}, "t1");
t1.start();
Thread.sleep(500);
log.debug("主线程打断t1线程");
t1.interrupt(); //打断t1线程的睡眠,并清除打断状态
Thread.sleep(10); //阻塞的线程打断之后需要等会会被清除打断状态
log.debug("线程t1的状态为{}",t1.isInterrupted()); //false
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("t1线程任务开始循环...");
while (true){
boolean flag = Thread.currentThread().isInterrupted();
//如果打断状态为真,就停止程序
if(flag)
break;
}
}, "t1");
t1.start();
Thread.sleep(1000);
log.debug("主线程开始打断");
t1.interrupt(); //打断t1线程的睡眠,并清除打断状态
Thread.sleep(10); //正常运行的线程打断之后需要等会并不会会清除打断状态
log.debug("线程t1的状态为{}",t1.isInterrupted()); //true
}
小结
sleep,yiled,wait,join 对比
关于join的原理和这几个方法的对比:看这里
补充:
- sleep,join,yield,interrupted是Thread类中的方法
- wait/notify是object中的方法
sleep 不释放锁、释放cpu
join 释放锁、抢占cpu
yiled 不释放锁、释放cpu
wait 释放锁、释放cpu
终止模式之两阶段终止模式
Two Phase Termination,就是考虑在一个线程T1中如何优雅地终止另一个线程T2?这里的优雅指的是给T2一个料理后事的机会(如释放锁)。
如下所示:那么线程的isInterrupted()
方法可以取得线程的打断标记,
如果线程在睡眠sleep
期间被打断,打断标记是不会变的,为false,但是sleep
期间被打断会抛出异常,我们据此
手动设置打断标记为true
;
如果是在程序正常运行期间被打断的,那么打断标记就被自动设置为true
。
处理好这两种情况那我们就可以放心地来料理后事啦!
@Slf4j
public class Test01 {
public static void main(String[] args) throws InterruptedException {
TwoParseTermination twoParseTermination = new TwoParseTermination();
twoParseTermination.start();
Thread.sleep(3000); // 让监控线程执行一会儿
twoParseTermination.stop(); // 停止监控线程
}
}
@Slf4j
class TwoParseTermination {
private Thread thread;
public void start() {
thread = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
log.debug("线程结束。。正在料理后事中");
break;
}
try {
Thread.sleep(500);
log.debug("正在执行监控的功能");
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); //避免监控线程睡眠时被监控线程被打断
}
}
},"monitorThread");
thread.start();
}
public void stop() {
thread.interrupt();
}
}
park方法停止线程
@Slf4j
public class Test02 {
public static void main(String[] args) throws InterruptedException {
test3();
}
private static void test3() throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("park停止该线程...");
LockSupport.park();//停止线程后,该线程一直处于park状态,直到被打断
log.debug("unPark");
log.debug("打断状态{}", Thread.currentThread().isInterrupted());
LockSupport.park(); //此时该线程被打断,但因其正常运行不会标记为false,因此失效(标记为true即失效)
log.debug("unPark1");
log.debug("打断状态{}", Thread.interrupted()); //返回为假,park方法会再次生效,类似i++,此行为true,但是下行已改false
LockSupport.park();
log.debug("unPark2");
},"t1");
t1.start();
Thread.sleep(1000);
t1.interrupt(); //被打断后t1线程会恢复继续运行
}
守护线程
默认情况下,java进程需要等待所有的线程结束后才会停止。
但是有一种特殊的线程,叫做
守护线程
,在其他线程全部结束的时候即使守护线程还未结束代码未执行完java进程也会停止。
普通线程t1可以调用t1.setDeamon(true);
方法变成守护线程
注意
-
垃圾回收器线程就是一种守护线程
-
Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等
待它们处理完当前请求
线程的状态
五种状态
其划分主要是从操作系统的层面进行划分的
- 初始状态,仅仅是在语言层面上创建了线程对象,即
Thead thread = new Thead();
,还未与操作系统线程关联 - 可运行状态,也称就绪状态,指该线程已经被创建,与操作系统相关联,等待cpu给它分配时间片就可运行
- 运行状态,指线程获取了CPU时间片,正在运行
- 当CPU时间片用完,线程会转换至【可运行状态】,等待 CPU再次分配时间片,会导致我们前面讲到的上下文切换
- 阻塞状态
- 如果调用了阻塞API,如BIO读写文件,那么线程实际上不会用到CPU,不会分配CPU时间片,会导致上下文切换,进入【阻塞状态】
- 等待BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
- 与【可运行状态】的区别是,只要操作系统一直不唤醒线程,调度器就一直不会考虑调度它们,CPU就一直不会分配时间片
- 终止状态,表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
六种状态
这是从 Java API 层面来描述的,我们主要研究的就是这种。状态转换详情图:地址
根据 Thread.State 枚举,分为六种状态
-
NEW 跟五种状态里的初始状态是一个意思
-
RUNNABLE 是当调用了
start()
方法之后的状态,注意,Java API 层面的RUNNABLE
状态涵盖了操作系统层面的【可运行状态】、【运行状态】和【IO阻塞状态】(由于 BIO 导致的线程阻塞(如读取文件),在 Java 里无法区分,仍然认为是可运行) -
BLOCKED
,WAITING
,TIMED_WAITING
都是 Java API 层面对【阻塞状态】的细分, -
BLOCKED
- 这个状态,一般是线程等待获取一个锁,来继续执行下一步的操作
-
WAITING
- 一个线程处于等待另一个线程执行的时候,需要关注的是,这边的等待是没有时间限制的。
- Object. Wait() 等待另一个线程执行Object.notify()或者Object.notifyAll()
- Thread. Join() 等待该线程执行完成
- LockSupport.park() 在等待执行LockSupport.unpark(thread)
- 一个线程处于等待另一个线程执行的时候,需要关注的是,这边的等待是没有时间限制的。
-
TIMED_WAITING
等待一段时间之后,会唤醒线程去重新获取锁,区别WAITING永久等待
-
Thread.sleep(long)
Object.wait(long)
Thread. Join(long)
LockSupport.parkNanos()
LockSupport.parkUntil()
-
查看线程进程的方法
windows系统
-
任务管理器可以查看进程和线程数,也可以杀死进程
-
tasklist查看进程
-
taskkill杀死进程
常用参数
/F 指定强制终止进程。
/PID 指定要终止的进程的 PID。(使用 TaskList 取得 PID)
linux系统
-
ps -fe 查看运行的进程
ps -fe | grep ef 筛选名字含有ef的进程
-
top -H -p 【进程的PID】 查看当前进程的所有线程
Java
-
jps命令查看所有java进程
-
jstack 【PID】查看某个Java进程的所有线程状态
-
jconsole 来查看某个Java进程中线程的运行情况(图形界面)
栈与栈帧(Stack and Frames)
Java Virtual Machine Stacks(Java 虚拟机栈)
JVM中由堆,栈,方法区组成,其中栈内存被线程所使用,每个线程启动后,虚拟机就会为其分配一块栈内存。
- 每个栈由多个栈帧Frame组成,对应着每次方法调用所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的方法
线程上下文切换(Thread Context Switch)
因为以下一些原因导致CPU不再执行当前的线程,转而执行另一个线程的代码
- 线程的CPU时间片用完
- 垃圾回收
- 有更高优先级的线程需要执行
- 线程自己调用了sleep,yield,wait,join,synchronized,lock等方法
当Context Switch发送时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java中对应的就是程序计数器(Program Counter Register),其作用是记住下一条jvm指令的执行地址,是线程私有的。
- 状态包括程序计数器,虚拟栈中每个栈帧的信息,如局部变量,操作数栈,返回地址等。
- Context Switch 频繁发送时会影响性能。