1. 进程与线程
1.1 进程
- 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的
- 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
- 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器
等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)
1.2 线程。
- 一个进程之内可以分为一到多个线程。
- 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行
- Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作
为线程的容器
1.3 二者对比
-
进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
-
进程拥有共享的资源,如内存空间等,供其内部的线程共享
-
进程间通信较为复杂
- 同一台计算机的进程通信称为 IPC(Inter-process communication)
- 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
-
线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
-
线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
2. 并行与并发
单核 cpu 下,线程实际还是 串行执行 的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感觉是同时运行的 。总结为一句话就是: 微观串行,宏观并行 。
一般会将这种线程轮流使用 CPU 的做法称为并发(concurrent)
多核 cpu下,每个 核(core) 都可以调度运行线程,这时候线程可以是并行的。
引用 Rob Pike 的一段描述:
- 并发(concurrent)是同一时间应对(dealing with)多件事情的能力
- 并行(parallel)是同一时间动手做(doing)多件事情的能力
例子 - 家庭主妇做饭、打扫卫生、给孩子喂奶,她一个人轮流交替做这多件事,这时就是并发
- 家庭主妇雇了个保姆,她们一起这些事,这时既有并发,也有并行(这时会产生竞争,例如锅只有一口,一
个人用锅时,另一个人就得等待)
3. 同步和异步
4. 创建和运行线程
4.1 直接使用 Thread
Thread thread = new Thread() {
@Override
public void run() {
//要执行的任务
log.debug("Here is some DEBUG");
log.info("Here is some INFO");
log.warn("Here is some WARN");
log.error("Here is some ERROR");
log.fatal("Here is some FATAL");
}
};
//线程启动
thread.start();
12:28:39 [Thread-2] DEBUG com.it.Main - Here is some DEBUG
12:28:39 [Thread-2] INFO com.it.Main - Here is some INFO
12:28:39 [Thread-2] WARN com.it.Main - Here is some WARN
12:28:39 [Thread-2] ERROR com.it.Main - Here is some ERROR
12:28:39 [Thread-2] FATAL com.it.Main - Here is some FATAL
//为线程指定的名字
Thread thread = new Thread("T1") {
@Override
public void run() {
//要执行的任务
log.debug("Here is some DEBUG");
log.info("Here is some INFO");
log.warn("Here is some WARN");
log.error("Here is some ERROR");
log.fatal("Here is some FATAL");
}
};
//线程启动
thread.start();
4.2 使用 Runnable 配合 Thread
把【线程】和【任务】(要执行的代码)分开
Thread 代表线程
Runnable 可运行的任务(线程要执行的代码)
Runnable runnable = new Runnable() {
public void run() {
//要执行的任务
log.debug("Here is some DEBUG");
log.info("Here is some INFO");
log.warn("Here is some WARN");
log.error("Here is some ERROR");
log.fatal("Here is some FATAL");
}
};
Thread thread = new Thread(runnable);
thread.start();
Runnable runnable = new Runnable() {
public void run() {
//要执行的任务
log.debug("Here is some DEBUG");
log.info("Here is some INFO");
log.warn("Here is some WARN");
log.error("Here is some ERROR");
log.fatal("Here is some FATAL");
}
};
Thread thread = new Thread(runnable,"T1");
thread.start();
4.3 lambda
Runnable runnable = () -> {
log.debug("Here is some DEBUG");
log.info("Here is some INFO");
log.warn("Here is some WARN");
log.error("Here is some ERROR");
log.fatal("Here is some FATAL");
};
new Thread(runnable, "T1").start();
Thread 与 Runnable 的关系
new Thread(runnable, "T1").start();
class Thread implements Runnable {
}
如果Runnable方法存在,则执行Runnable中的方法,否则执行默认run方法
@Override
public void run() {
if (target != null) {
target.run();
}
}
4.4 FutureTask 配合 Thread
package com.it;
import org.apache.log4j.Logger;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @author
* @date 2022/10/17 12:14
* @remark
*/
public class Main {
private static final Logger log = Logger.getLogger(Main.class);
public static void main(String[] args) {
FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.debug("runing");
Thread.sleep(2000);
return 100;
}
});
new Thread(task,"T1").start();
//阻塞结果返回
try{
Integer integer = task.get();
log.debug(integer);
}catch (InterruptedException | ExecutionException e){
System.out.println(e);
}
}
}
13:07:46:572 [T1] DEBUG com.it.Main -runing
13:07:48:578 [main] DEBUG com.it.Main -100
5. 常见方法
5.1 run和start对比
-
start()
- 启动一个新线程,在新的线程运行 run 方法中的代码
- start 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。每个线程对象的start方法只能调用一次,如果调用了多次会出现IllegalThreadStateException
-
run():
- 新线程启动后会调用的方法
- 如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象,来覆盖默认行为。
-
小结
- 直接调用 run 是在主线程中执行了 run,没有启动新的线程
- 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码
5.2 sleep与yield对比
-
yield()
- static
- 功能说明:提示线程调度器让出当前线程对CPU的使用
- 注意主要是为了测试和调试
-
sleep(long n)
- static
- 让当前执行的线程休眠n毫秒,休眠时让出 cpu 的时间片给其它线程
-
sleep
- 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
- 睡眠结束后的线程未必会立刻得到执行
- 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
- yield
- 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
- 具体的实现依赖于操作系统的任务调度器
@Test
public void test6() {
Thread thread = new Thread("T1"){
@Override
public void run() {
try {
Thread.sleep(2000);
//可能会被打断
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
//15:04:21:724 [main] DEBUG someTest -t1 state:RUNNABLE
//子线程还没执行睡眠操作
thread.start();
log.debug("t1 state:"+thread.getState());
//让主线程休眠
try {
Thread.sleep(500);
//可能会被打断
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("t1 state:"+thread.getState());
}
15:05:50:059 [main] DEBUG someTest -t1 state:RUNNABLE
15:05:50:564 [main] DEBUG someTest -t1 state:TIMED_WAITING
@Test
public void testInterrupted() throws InterruptedException {
Thread t1 = new Thread("T1"){
@Override
public void run() {
try {
log.debug("t1 enter sleep");
Thread.sleep(2000);
//可能会被打断
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("t1 wake up");
}
}
};
t1.start();
Thread.sleep(1000);
log.debug("t1 interrupt");
//打断睡眠
t1.interrupt();
}
15:13:53:275 [T1] DEBUG someTest -t1 enter sleep
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at someTest$3.run(someTest.java:86)
15:13:54:275 [main] DEBUG someTest -t1 interrupt
15:13:54:276 [T1] DEBUG someTest -t1 wake up
sleep可读性
TimeUnit.SECONDS.sleep(1);
线程优先级
线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
5.3 join
为什么需要 join
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug("开始");
sleep(1);
log.debug("结束");
r = 10;
});
t1.start();
log.debug("结果为:{}", r);
log.debug("结束");
}
- 分析
- 因为主线程和线程 t1 是并行执行的,t1 线程需要 1 秒之后才能算出 r=10
- 而主线程一开始就要打印 r 的结果,所以只能打印出 r=0
- 解决方法
- 用 sleep 行不行?为什么?肯定行,不知道要休眠多久
- 用 join,加在 t1.start() 之后即可
等待一定时间
t1.join(1500);
5.4 interrupt()和isInterrupted()
sleep,wait,join 都是阻塞线程
- isInterrupted() 不会清除 打断标记
- interrupt() 如果被打断线程正在 sleep,wait,join 会导致被打断
的线程抛出 InterruptedException,并清除 打断标记 ;如果打断的正在运行的线程,则会设置 打断标记 ;park 的线程被打断,也会设置 打断标记
5.4.1 打断 sleep 的线程
@Test
public void test7() {
Thread t1 = new Thread(() -> {
try {
log.debug("sleep");
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1");
t1.start();
log.debug(" 打断");
t1.interrupt();
log.debug(" 打断标记: {}" + t1.isInterrupted());
}
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at someTest.lambda$test7$0(someTest.java:108)
at java.lang.Thread.run(Thread.java:748)
15:51:08:152 [t1] DEBUG someTest -sleep
15:51:08:152 [main] DEBUG someTest - 打断
15:51:08:152 [main] DEBUG someTest - 打断标记: {}false
Process finished with exit code 0
5.4.2 打断正常运行的线程
@Test
public void test8() throws InterruptedException {
Thread t1 = new Thread(() -> {
while (true){
boolean interrupted = Thread.currentThread().isInterrupted();
if(interrupted){
log.info("死循环退出了");
break;
}
}
}, "t1");
t1.start();
log.debug(" 打断");
Thread.sleep(1000);
t1.interrupt();
log.debug(" 打断标记: {}" + t1.isInterrupted());
}
15:54:23:175 [main] DEBUG someTest - 打断
15:54:23:176 [main] DEBUG someTest - 打断标记: {}true
终止模式之两阶段终止模式:Two Phase Termination
目标:在一个线程 T1 中如何优雅终止线程 T2
错误思想:
- 使用线程对象的 stop() 方法停止线程:stop 方法会真正杀死线程,如果这时线程锁住了共享资源,当它被杀死后就再也没有机会释放锁,其它线程将永远无法获取锁
- 使用 System.exit(int) 方法停止线程:目的仅是停止一个线程,但这种做法会让整个程序都停止
public class Test {
public static void main(String[] args) throws InterruptedException {
TwoPhaseTermination tpt = new TwoPhaseTermination();
tpt.start();
Thread.sleep(3500);
tpt.stop();
}
}
class TwoPhaseTermination {
private Thread monitor;
// 启动监控线程
public void start() {
monitor = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
Thread thread = Thread.currentThread();
if (thread.isInterrupted()) {
System.out.println("后置处理");
break;
}
try {
Thread.sleep(1000); // 睡眠
System.out.println("执行监控记录"); // 在此被打断不会异常
} catch (InterruptedException e) { // 在睡眠期间被打断,进入异常处理的逻辑
e.printStackTrace();
// 重新设置打断标记,打断 sleep 会清除打断状态
thread.interrupt();
}
}
}
});
monitor.start();
}
// 停止监控线程
public void stop() {
monitor.interrupt();
}
}
不推荐方法
不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁:
-
public final void stop():停止线程运行
废弃原因:方法粗暴,除非可能执行 finally 代码块以及释放 synchronized 外,线程将直接被终止,如果线程持有 JUC 的互斥锁可能导致锁来不及释放,造成其他线程永远等待的局面 -
public final void suspend():挂起(暂停)线程运行
废弃原因:如果目标线程在暂停时对系统资源持有锁,则在目标线程恢复之前没有线程可以访问该资源,如果恢复目标线程的线程在调用 resume 之前会尝试访问此共享资源,则会导致死锁
-
public final void resume():恢复线程运行
守护线程
默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
log.debug("开始运行...");
Thread t1 = new Thread(() -> {
log.debug("开始运行...");
sleep(2);
log.debug("运行结束...");
}, "daemon");
// 设置该线程为守护线程
t1.setDaemon(true);
t1.start();
sleep(1);
log.debug("运行结束...");
注意
- 垃圾回收器线程就是一种守护线程
- Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求
五种状态
- 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联
- 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
- 【运行状态】指获取了 CPU 时间片运行中的状态
当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换 - 【阻塞状态】
- 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入【阻塞状态】
- 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
- 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们
- 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
六种状态
从 Java API 层面来描述的,根据 Thread.State 枚举,分为六种状态
- 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
- 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。 - 阻塞(BLOCKED):表示线程阻塞于锁。
- 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
- 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
- 终止(TERMINATED):表示该线程已经执行完毕。
- NEW 线程刚被创建,但是还没有调用 start() 方法
- RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面RUNNABLE 状态涵盖了 操作系统 层面的【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
- BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节详述
- TERMINATED 当线程代码运行结束
泡茶统筹规划
package com.it;
import org.apache.log4j.Logger;
/**
* @author
* @date 2022/10/17 16:44
* @remark
*/
public class TestState {
private static final Logger log = Logger.getLogger(TestState.class);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug("running...");
}
};
Thread t2 = new Thread("t2") {
@Override
public void run() {
while (true) {
}
}
};
t2.start();
Thread t3 = new Thread("t3") {
@Override
public void run() {
log.debug("running...");
}
};
t3.start();
Thread t4 = new Thread("t4") {
@Override
public void run() {
synchronized(TestState.class){
try {
Thread.sleep(1000000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t4.start();
//waiting
Thread t5 = new Thread("t5") {
@Override
public void run() {
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t5.start();
//拿不到锁 block
Thread t6 = new Thread("t6") {
@Override
public void run() {
synchronized(TestState.class){
}
}
};
t6.start();
Thread.sleep(500);
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());
}
}
17:05:40:796 [main] DEBUG com.it.TestState -t1 state NEW
17:05:40:796 [main] DEBUG com.it.TestState -t2 state RUNNABLE
17:05:40:796 [main] DEBUG com.it.TestState -t3 state TERMINATED
17:05:40:796 [main] DEBUG com.it.TestState -t4 state TIMED_WAITING
17:05:40:796 [main] DEBUG com.it.TestState -t5 state WAITING
17:05:40:796 [main] DEBUG com.it.TestState -t6 state BLOCKED
package com.it;
import org.apache.log4j.Logger;
import java.util.concurrent.TimeUnit;
/**
* @author
* @date 2022/10/17 17:16
* @remark
*/
public class Tea {
private static final Logger log = Logger.getLogger(Tea.class);
public static void main(String[] args) {
Thread t1 = new Thread("老王") {
@Override
public void run() {
log.debug("洗水壶");
Sleeper.sleepMin(1);
log.debug("烧开水");
Sleeper.sleepMin(5);
}
};
t1.start();
Thread t2 = new Thread("小李") {
@Override
public void run() {
log.debug("洗茶壶");
Sleeper.sleepMin(1);
log.debug("洗茶杯");
Sleeper.sleepMin(2);
log.debug("拿茶叶");
Sleeper.sleepMin(4);
try {
t1.join();
log.debug("泡茶");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t2.start();
}
}
17:24:02:225 [小李] DEBUG com.it.Tea -洗茶壶
17:24:02:225 [老王] DEBUG com.it.Tea -洗水壶
17:24:03:228 [老王] DEBUG com.it.Tea -烧开水
17:24:03:228 [小李] DEBUG com.it.Tea -洗茶杯
17:24:05:241 [小李] DEBUG com.it.Tea -拿茶叶
17:24:09:245 [小李] DEBUG com.it.Tea -泡茶
思考:
- 上面是小李等待老王的水烧开了,如果反过来,老王等小李的茶叶拿来,什么代码能适应两种情况
- 上面线程其实是各执行各的,数据之间没有共享