并发
1.进程与线程
1.1 进程
- 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的。
- 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
- 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器 等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)
1.2 线程
- 一个进程之内可以分为一到多个线程。
- 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行 。
- Java 中,线程作为小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作 为线程的容器
1.3 两者对比
-
进程基本上相互独立的,而线程存在于进程内,是进程的一个子集 -
进程拥有共享的资源,如内存空间等,供其内部的线程共享 -
进程间通信较为复杂
-
线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量 -
线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
2. 并行和并发
2.1 并发
2.2并行
2.3 应用(异步)
-
以调用方角度来讲,如果 -
需要等待结果返回,才能继续运行就是同步 -
不需要等待结果返回,就能继续运行就是异步 -
设计
- 多线程可以让方法执行变为异步的(即不要巴巴干等着)比如说读取磁盘文件时,假设读取操作花费了 5 秒钟,如 果没有线程调度机制,这 5 秒 cpu 什么都做不了,其它代码都得暂停…
-
结论
- 比如在项目中,视频文件需要转换格式等操作比较费时,这时开一个新线程处理视频转换,避免阻塞主线程
- tomcat 的异步 servlet 也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞 tomcat 的工作线程
- ui 程序中,开线程进行其他操作,避免阻塞 ui 线程
-
提高效率
- 单核 cpu 下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用 cpu ,不至于一个线程总占用 cpu,别的线程没法干活
- 多核 cpu 可以并行跑多个线程,但能否提高程序运行效率还是要分情况的 有些任务,经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率。但不是所有计算任 务都能拆分(参考后文的【阿姆达尔定律】) 也不是所有任务都需要拆分,任务的目的如果不同,谈拆分和效率没啥意
- IO 操作不占用 cpu,只是我们一般拷贝文件使用的是【阻塞 IO】,这时相当于线程虽然不用 cpu,但需要一 直等待 IO 结束,没能充分利用线程。所以才有后面的【非阻塞 IO】和【异步 IO】优化,(这种情况虽然不占用CPU,但是会使线程一直阻塞,使线程利用率降低,而非阻塞和异步可以有效地提升线程的利用率);CPU只需要通知DMA就可以,然后等到数据准备好之后触发中断,告知CPU数据准备好了
3. JAVA线程
3.1 创建和运行线程
方法一:直接使用Thread
方法二:使用Runnable
-
把线程和任务(要执行的代码)分开 -
Thread代表线程 -
Runnable可运行的任务(线程要执行的代码)
-
package com.sunyang.concurrentstudy;
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.RunnableDemo")
public class RunnableDemo {
public static void main(String[] args) {
Runnable runnable = () -> log.debug("runnable-running");
Thread thread = new Thread(runnable, "thread-runnable");
thread.start();
log.debug("main-running");
}
}
Thread和Runnable联系
-
-
-
如果通过Runnable方法创建的线程,实际上是调用的Thread中的run() -
new Thread()是通过创建子类实现父类Thread的run() 方法 -
Runnable属于组合方式,new Thread属于继承方式 组合优于继承 -
方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开了 -
用 Runnable 更容易与线程池等高级 API 配合 -
用 Runnable 让任务类脱离了 Thread 继承体系,更灵活
方法三:FutureTask
-
package com.sunyang.concurrentstudy;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
@Slf4j(topic = "c.FutureTaskDemo")
public class FutureTaskDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.debug("running-thread");
Thread.sleep(2000);
return 100;
}
});
Thread thread = new Thread(task, "task");
thread.start();
log.debug("{}", task.get());
}
}
-
Connected to the target VM, address: '127.0.0.1:59755', transport: 'socket'
19:53:51 [task] c.FutureTaskDemo - running-thread
19:53:53 [main] c.FutureTaskDemo - 100
Disconnected from the target VM, address: '127.0.0.1:59755', transport: 'socket'
Process finished with exit code 0
3.2 线程运行原理
线程上下文切换
-
因为一下一些原因导致CPU不再执行当前的线程,转而执行另一个线程的代码
- 线程的CPU时间片用完了
- 垃圾回收(看垃圾回收器,正常时FullGC)
- 有更高优先级的线程需要执行
- 线程自己调用了sleep, yield, wait, join, park, synchronized,lock等方法
-
当Context Switch发生时,需要操作系统保存当前线程的状态(保护现场),并恢复另一个线程的状态,Java中对应的概念就是程序计数器,他的作用是记录下一条JVM指令的执行地址,是线程私有的 -
线程状态包括:程序计数器,虚拟机栈中的每个栈帧的信息,如局部变量,操作数栈,返回地址,动态链接等 -
Context Switch 频繁发生会影响性能
3.3 常见方法
3.3.1 start与run
-
直接调用run() 就是调用的普通方法 -
package com.sunyang.concurrentstudy;
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.RunDemo")
public class RunDemo {
public static void main(String[] args) {
Thread thread = new Thread(){
@Override
public void run() {
log.debug("run");
}
};
thread.run();
}
}
-
结果 -
20:45:16 [main] c.RunDemo - run
Process finished with exit code 0
3.3.2 sleep与yield
sleep
-
调用sleep会让当前线程从Running 状态进入到Timed Waiting(有时限的等待)状态
-
package com.sunyang.concurrentstudy;
import lombok.extern.slf4j.Slf4j;
/**
* @program: ConcurrentStudy
* @description: Dmeo
* @author: SunYang
* @create: 2021-07-28 21:05
**/
@Slf4j(topic = "c.Demo")
public class ThreadSleepDemo {
public static void main(String[] args) {
Thread thread = new Thread("t1"){
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
log.debug("{}", thread.getState()); // 21:12:07 [main] c.Demo - NEW
thread.start();
log.debug("{}", thread.getState()); // 21:12:07 [main] c.Demo - RUNNABLE
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("{}", thread.getState()); // 21:12:08 [main] c.Demo - TIMED_WAITING
}
}
// 输出
// 21:12:07 [main] c.Demo - NEW
// 21:12:07 [main] c.Demo - RUNNABLE
// 21:12:08 [main] c.Demo - TIMED_WAITING
-
其他线程可以使用interrupt方法打断正在睡眠的线程,这时sleep方法会抛出InterruptedException
-
package com.sunyang.concurrentstudy;
import lombok.extern.slf4j.Slf4j;
/**
* @program: ConcurrentStudy
* @description: Demo
* @author: SunYang
* @create: 2021-07-28 21:15
**/
@Slf4j(topic = "c.Demo")
public class SleepInterruptDemo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("enter sleep");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
log.debug("wake up ....");
e.printStackTrace();
}
}, "t1"
);
t1.start();
Thread.sleep(1000);
log.debug("interrupt..");
t1.interrupt();
}
}
-
输出 -
21:20:41 [t1] c.Demo - enter sleep
21:20:42 [main] c.Demo - interrupt..
21:20:42 [t1] c.Demo - wake up ....
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.sunyang.concurrentstudy.SleepInterruptDemo.lambda$main$0(SleepInterruptDemo.java:17)
at java.lang.Thread.run(Thread.java:748)
Process finished with exit code 0
-
睡眠结束后的线程未必会立刻得到执行 -
建议用TimeUnit的sleep来代替Thread的sleep来获得更好的可读性
yield
- 调用yeild会让当前线程从Running(运行)转为Runnable(就绪)状态,然后调度其他线程
- 具体的实现依赖于操作系统的任务调度器(如果当前没有别的线程,那么还会继续执行这个线程,或者他刚让出CPU的使用权,立刻就又获得了使用权,那么还会继续执行)
区别
- sleep(阻塞状态)不能获得CPU时间片,分配时间片时不会考虑阻塞状态
- yeild(就绪状态)有机会获得CPU时间片
线程优先级
- 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽视它
- 如果CPU比较忙,那么优先级高的线程可能会获得更多的时间片,但CPU闲时,优先级几乎没用
- yeild也是如此。
sleep使用
- 在没有利用CPU来计算时,不要让while(true)空转浪费CPU,这时可以使用yeild和sleep来让出CPU的使用权给其他程序(防止CPU占用100%,单核情况下)
- 可以用wait或条件变量达到类似效果,
- 不同的是,后两种都需要加锁,并且需要相应的唤醒操作,一般适用于要进行同步的场景
- sleep适用于无锁同步的场景
3.3.3 join
-
package com.sunyang.concurrentstudy;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
@Slf4j(topic = "c.Demo")
public class JoinDemo {
static int r = 0;
public static void main(String[] args) {
Thread thread = new Thread(() -> {
log.debug("开始");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
r = 10;
log.debug("结束");
});
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("结果{}", r);
}
}
-
应用之同步
- 需要等待结果返回,才能继续运行就是i同步
- 不需要等待结果返回,就能继续运行,就是异步
有时效的join
-
如果等够时间还没返回,则继续执行 -
package com.sunyang.concurrentstudy;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
@Slf4j(topic = "c.Demo")
public class JoinDemo {
static int r = 0;
public static void main(String[] args) {
Thread thread = new Thread(() -> {
log.debug("开始");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
r = 10;
log.debug("结束");
});
thread.start();
try {
thread.join(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("结果{}", r);
}
}
-
如果线程提前结束,那么会直接返回结果,不用等待足够的秒数
- thread.join(3000); // 两秒后返回结果了,就继续向下运行,不用等到3秒再继续
3.3.4 interrupt
- 除了可以打断处于阻塞状态的线程,其他正在运行的线程也可以打断
打断阻塞的线程
-
打断sleep,wait,join的线程 -
打断sleep的线程,会清空打断状态,以sleep为例 -
package com.sunyang.concurrentstudy;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
@Slf4j(topic = "c.Demo")
public class SleepInterruptDemoT {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
log.debug("sleep..");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1");
thread.start();
log.debug("interrupt...");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();
log.debug("打断标记被清除--{}", thread.isInterrupted());
}
}
-
输出 -
22:31:30 [main] c.Demo - interrupt...
22:31:30 [t1] c.Demo - sleep..
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at com.sunyang.concurrentstudy.SleepInterruptDemoT.lambda$main$0(SleepInterruptDemoT.java:19)
at java.lang.Thread.run(Thread.java:748)
22:31:31 [main] c.Demo - 打断标记被清除--false
Disconnected from the target VM, address: '127.0.0.1:62851', transport: 'socket'
Process finished with exit code 0
-
处于Running状态的线程被打断后,不会直接停止运行,而是由被打断线程自己决定,是否停止自己的线程,让出cpu,可以优雅的停止线程,在停止线程之前可以做一些善后工作。 -
package com.sunyang.concurrentstudy;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
@Slf4j(topic = "c.Demo")
public class SleepInterruptDemoT {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (true){
if (Thread.currentThread().isInterrupted()) {
log.debug("被打断了");
break;
}
log.debug("虽然interrupt我了,但是我还能执行。。");
}
}, "t1");
thread.start();
TimeUnit.SECONDS.sleep(1);
log.debug("interrupt..");
thread.interrupt();
log.debug("打断状态--{}", thread.isInterrupted());
}
}
-
22:44:30 [t1] c.Demo - 虽然interrupt我了,但是我还能执行。。
22:44:30 [t1] c.Demo - 虽然interrupt我了,但是我还能执行。。
22:44:30 [t1] c.Demo - 虽然interrupt我了,但是我还能执行。。
22:44:30 [t1] c.Demo - 虽然interrupt我了,但是我还能执行。。
22:44:30 [t1] c.Demo - 虽然interrupt我了,但是我还能执行。。
22:44:30 [t1] c.Demo - 虽然interrupt我了,但是我还能执行。。
22:44:30 [t1] c.Demo - 虽然interrupt我了,但是我还能执行。。
22:44:30 [t1] c.Demo - 虽然interrupt我了,但是我还能执行。。
22:44:30 [t1] c.Demo - 虽然interrupt我了,但是我还能执行。。
22:44:30 [main] c.Demo - interrupt..
22:44:30 [t1] c.Demo - 虽然interrupt我了,但是我还能执行。。
22:44:30 [t1] c.Demo - 被打断了
22:44:30 [main] c.Demo - 打断状态--true
模式之两阶段终止模式
-
使用线程对象的stop()方法停止线程(废弃)
- stop方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其他线程就永远无法获取锁吗,就会造成死锁。
-
使用System.exit(int)方法停止线程
- 目的仅是停止一个线程,但这种做法会让整个程序都停止
两阶段终止
-
-
场景:记录电脑健康状态CPU使用率等等,搞一个后台的监控线程,while(true)循环,每两秒记录一下,并且要有一个打断按钮,不想监控时,可以停下。 -
如果在睡眠状态被打断,会抛出异常,但是打断标记会被清除设置为false,所以要自己手动设置打断标记。 -
package com.sunyang.concurrentstudy;
import ch.qos.logback.core.util.TimeUtil;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
@Slf4j(topic = "c.Demo")
public class TwoPhaseTerminationDemo {
public static void main(String[] args) {
TwoPhaseTermination twoPhaseTermination = new TwoPhaseTermination();
twoPhaseTermination.start();
try {
TimeUnit.SECONDS.sleep(3);
twoPhaseTermination.stop();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Slf4j(topic = "c.TwoDemo")
class TwoPhaseTermination {
private Thread monitor;
public void start() {
monitor = new Thread(() -> {
while (true) {
Thread current = Thread.currentThread();
if (current.isInterrupted()) {
log.debug("善后工作!");
break;
}
try {
TimeUnit.SECONDS.sleep(1);
log.debug("执行监控记录");
} catch (InterruptedException e) {
e.printStackTrace();
current.interrupt();
}
}
});
monitor.start();
}
public void stop() {
monitor.interrupt();
}
}
-
11:02:32 [Thread-0] c.TwoDemo - 执行监控记录
11:02:33 [Thread-0] c.TwoDemo - 执行监控记录
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at com.sunyang.concurrentstudy.TwoPhaseTermination.lambda$start$0(TwoPhaseTerminationDemo.java:41)
at java.lang.Thread.run(Thread.java:748)
11:02:34 [Thread-0] c.TwoDemo - 善后工作!
打断park线程
-
打断park线程,不会清空打断状态(不是Thread中的方法,是LockSupport.park()(锁的支持类中的方法)) -
package com.sunyang.concurrentstudy;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
@Slf4j(topic = "c.Demo")
public class ParkDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
log.debug("park。。。。");
LockSupport.park();
log.debug("unpark...");
log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
LockSupport.park();
log.debug("isInterrupted为true情况下执行了park()方法,但是依然继续执行,park()方法未生效");
}, "t1");
thread.start();
TimeUnit.SECONDS.sleep(1);
log.debug("thread状态:{}", thread.getState());
thread.interrupt();
}
}
-
11:26:58 [t1] c.Demo - park。。。。
11:26:59 [main] c.Demo - thread状态:WAITING
11:26:59 [t1] c.Demo - unpark...
11:26:59 [t1] c.Demo - 打断状态:true
11:26:59 [t1] c.Demo - isInterrupted为true情况下执行了park()方法,但是依然继续执行,park()方法未生效
3.4 主线程和守护线程
-
默认情况下,java进程需要等待所有的线程都运行结束,才会结束,但是有一种特殊的线程,叫做守护线程,只要其他非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。 -
package com.sunyang.concurrentstudy;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
@Slf4j(topic = "c.Demo")
public class DaemonThreadDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while(true) {
if (Thread.currentThread().isInterrupted()){
break;
}
}
log.debug("结束");
}, "t1");
thread.setDaemon(true);
thread.start();
TimeUnit.SECONDS.sleep(1);
log.debug("结束");
}
}
-
13:38:56 [main] c.Demo - 结束
Process finished with exit code 0
-
垃圾回收器就是一种守护线程 -
Tomcat中的Acceptor和Poller线程都是守护线程,所以Tomcat接收到shutdown命令后,不会等待他们处理完请求。
3.5 五种状态
3.6 六种状态
- 从Java API 层面来描述
- 根据Thread.State枚举,分为六种状态
API
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
-
-
初始,可运行,阻塞,等待,超时等待,终止 -
【NEW】
-
【RUNNABLE】
- 当调用了start()方法之后,注意,Java API 层面的RUNNABLE状态涵盖了操作系统层面的**【可运行状态】,【运行状态】和【阻塞状态】**
- 由于BIO导致的线程阻塞,再JAVA里无法区分,仍然认为是可运行
- **【BLOCKED】,【WAITING】, 【TIMED_WAITING】都是Java API 层面对【阻塞状态】**的细分。
-
【TERMINATED】 -
当线程代码结束运行 -
【BLOCKED】 -
【WAITING】
-
【TIMED_WAITING】 -
sleep -
package com.sunyang.concurrentstudy;
import lombok.extern.slf4j.Slf4j;
import java.sql.Time;
import java.util.concurrent.TimeUnit;
@Slf4j(topic = "c.Demo")
public class ThreadSixStateDemo {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("running.....");
}, "t1");
Thread t2 = new Thread(() -> {
while (true){
}
});
t2.start();
Thread t3 = new Thread(() -> {
log.debug("running.....");
});
t3.start();
Thread t4 = new Thread(() -> {
synchronized (ThreadSixStateDemo.class){
try {
TimeUnit.SECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t4.start();
Thread t5 = new Thread(() -> {
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t5.start();
Thread t6 = new Thread(() -> {
synchronized (ThreadSixStateDemo.class){
try {
TimeUnit.SECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t6.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("t1 state {}", t1.getState());
log.debug("t2 state {}", t2.getState());
log.debug("t3 state {}", t3.getState());
log.debug("t4 state {}", t4.getState());
log.debug("t5 state {}", t5.getState());
log.debug("t6 state {}", t6.getState());
}
}
-
16:56:11 [Thread-1] c.Demo - running.....
16:56:12 [main] c.Demo - t1 state NEW
16:56:12 [main] c.Demo - t2 state RUNNABLE
16:56:12 [main] c.Demo - t3 state TERMINATED
16:56:12 [main] c.Demo - t4 state TIMED_WAITING
16:56:12 [main] c.Demo - t5 state WAITING
16:56:12 [main] c.Demo - t6 state BLOCKED
|