CPU
,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO
的。CPU
执行。Java
中,线程作为最小调度单位,进程作为资源分配的最小单位。 在 windows
中进程是不活动的,只是作为线程的容器。IPC
(Inter-process communication)。HTTP
。cpu
下,线程都是串行执行的。cpu
的时间片分给不同的程序使用,只是由于 cpu
在线程间的切换非常快,所以给人的感觉是同时运行的。cpu
条件下,线程微观串行,宏观并行。cpu
的做法成为并发(concurrent)。+ - - - - -+
' cpu: '
' '
' +------+ ' +----------------------------+
' | core | ' --> | instruction-sets(thread-1) |
' +------+ ' +----------------------------+
' '
+ - - - - -+
cpu
下,每个核(core
) 都可以调度运行线程,这时候线程可以是并行(Parallel)的。+ - - - - - -+
' cpu: '
' '
' +--------+ ' +----------------------------+
' | core-1 | ' --> | instruction-sets(thread-1) |
' +--------+ ' +----------------------------+
' +--------+ ' +----------------------------+
' | core-2 | ' --> | instruction-sets(thread-2) |
' +--------+ ' +----------------------------+
' +--------+ ' +----------------------------+
' | core-3 | ' --> | instruction-sets(thread-3) |
' +--------+ ' +----------------------------+
' +--------+ ' +----------------------------+
' | core-4 | ' --> | instruction-sets(thread-4) |
' +--------+ ' +----------------------------+
' '
+ - - - - - -+
并发和并行是两个不同的概念。
借用Go创始人Rob Pike的说法:
并发(concurrent)是同一时间应对(dealing with)多件事情的能力。
并行(parallel)是同一时间动手做(doing)多件事情的能力
举个简单的例子:华罗庚泡茶,必须有烧水、洗杯子、拿茶叶等步骤,现在我们想尽快做完这件事,也就是“一共要处理很多事情”,有很多方法可以实现并发,例如请多个人同时做,这就是并行。
并行是实现并发的一种方式,但不是唯一的方式。我们一个人也可以实现并发,例如先烧水、然后不用等水烧开就去洗杯子,所以通过调整程序运行方式也可以实现并发。
cpu
下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用 cpu
,避免一个线程总占用 cpu
,别的线程没法干活。cpu
可以并行跑多个线程,但能否提高程序运行效率还是要分情况的:
IO
操作不占用 cpu
,只是我们一般拷贝文件使用的是【阻塞 IO
】,这时相当于线程虽然不用 cpu
,但需要一直等待 IO
结束,没能充分利用线程。所以才会涉及到【非阻塞 IO
】和【异步 IO
】优化。 /**
* 方式一:直接使用 Thread 对象进行创建。
*/
@Test
public void test01() {
// 1.显式创建线程,并指定线程名。
Thread t1 = new Thread("t1") {
@Override
public void run() {
System.out.println(" t1线程 执行 ");
}
};
// 2.启动线程。
t1.start();
// t1线程 执行
}
/**
* 方式二:使用 Runnable + Thread 进行创建。
*/
@Test
public void test02() {
// 1.创建任务对象。
Runnable task = () -> System.out.println(" 任务执行 ");
// 2.创建线程 (传入任务 及 线程名)。
Thread t2 = new Thread(task, "t2");
t2.start();
// 任务执行
}
/**
* FutureTask 配合 Thread。
*
* @throws ExecutionException 执行异常
* @throws InterruptedException 中断异常
*/
@Test
public void test03() throws ExecutionException, InterruptedException {
// 1.创建任务对象 (接收 Callable 类型的参数 及 返回值)。
FutureTask<String> task = new FutureTask<>(
() -> System.out.println(" 任务执行 "),
"hello");
// 2.创建线程 (传入任务 及 线程名)。
Thread t3 = new Thread(task, "t3");
t3.start();
// 任务执行
// 3.主线程阻塞,同步等待 task 执行完毕的结果。
System.out.println("获取返回值:" + task.get());
// 获取返回值:hello
}
Runnable
更容易与线程池等高级 API
配合。Runnable
让任务类脱离了 Thread
继承体系,更灵活。任务管理器可以查看进程和线程数,也可以用来杀死进程。
tasklist
查看进程:
# 查看java进程。
tasklist | findstr "java"
# 查询进程使用的端口号。
netstat -ano | find "PID"
taskkill
杀死进程:# 根据pid终止进程。
taskkill /f /pid 123456
# 根据进程的名称终止进程。
taskkill /f /im xxx.exe
ps -ef
查看所有进程。# 查看java进程。
$ ps -ef | grep java
ps -fT -p PID
查看某个进程(PID
)的所有线程。
kill
杀死进程。
top
按大写 H
切换是否显示线程。
top -H -p PID
查看某个进程(PID
)的所有线程。
jps
命令查看所有 Java
进程。
jstack PID
查看某个 Java
进程(PID
)的所有线程状态。
jconsole
来查看某个 Java
进程中线程的运行情况(图形界面):
因为以下一些原因导致 cpu
不再执行当前的线程,转而执行另一个线程的代码。
cpu
时间片用完。sleep()
、yield()
、wait()
、join()
、park()
、synchronized()
、lock()
等方法。当线程上下文切换(Context Switch
)发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java
中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm
指令的执行地址,是线程私有的。
Context Switch
) 频繁发生会影响性能。方法名 | 是否为静态方法 | 功能说明 | 注意事项 |
---|---|---|---|
start() | 启动一个新线程,在新的线程运行 run 方法中的代码。 | start 方法只是让线程进入就绪,里面代码不一定立刻运行(cpu 的时间片还没分给它)。每个线程对象的 start 方法只能调用一次,如果调用了多次会出现IllegalThreadStateException 。 | |
run() | 新线程启动后会调用的方法。 | 如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作,但可以创建 Thread 的子类对象,来覆盖默认行为。 | |
join() | 等待线程运行结束。 | ||
join(long n) | 等待线程运行结束,最多等待 n 毫秒。 | ||
getId() | 获取线程长整型的 id 。 | id 唯一。 | |
getName() | 获取线程名。 | ||
setName(String) | 修改线程名。 | ||
getPriority() | 获取线程优先级。 | ||
setPriority(int) | 修改线程优先级。 | java中规定线程优先级是1~10 的整数,较大的优先级能提高该线程被 cpu 调度的机率。 | |
getState() | 获取线程状态。 | Java 中线程状态是用 6 个 enum 表示,分别为:NEW ,RUNNABLE , BLOCKED ,WAITING , TIMED_WAITING , TERMINATED 。 | |
isInterrupted() | 判断是否被打断。 | 不会清除 打断标记。 | |
isAlive() | 线程是否存活(还没有运行完毕)。 | ||
interrupt() | 打断线程。 | 如果被打断线程正在 sleep ,wait ,join 会导致被打断的线程抛出 InterruptedException ,并清除打断标记 ;如果打断的正在运行的线程,则会设置打断标记 ;park 的线程被打断,也会设置打断标记。 | |
interrupted() | 是 | 判断当前线程是否被打断。 | 会清除 打断标记。 |
currentThread() | 是 | 获取当前正在执行的线程。 | |
sleep(long n) | 是 | 让当前执行的线程休眠n毫秒,休眠时让出 cpu 的时间片给其它线程。 | |
yield() | 是 | 提示线程调度器让出当前线程对 cpu 的使用。 | 主要是为了测试和调试。 |
public static void main(String[] args) {
// 1.创建线程。
Thread t1 = new Thread("t1") {
@Override
public void run() {
try {
log.debug("当前执行的线程名:" + Thread.currentThread().getName());
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
// 2.使用run方法执行。
t1.run();
// [main] DEBUG 当前执行的线程名:main
log.debug("do other things...");
// [main] DEBUG do other things...
}
public static void main(String[] args) {
// 1.创建线程。
Thread t1 = new Thread("t1") {
@Override
public void run() {
try {
log.debug("当前执行的线程名:" + Thread.currentThread().getName());
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
// 2.使用start方法执行。
t1.start();
// [t1] DEBUG 当前执行的线程名:t1
log.debug("do other things...");
// [main] DEBUG do other things...
}
run
是在主线程中执行了 run
,没有启动新的线程。start
是启动新的线程,通过新的线程间接执行 run
中的代码。 @Test
public void test01() throws InterruptedException {
Thread t1 = new Thread(
() -> {
try {
log.debug("t1 进入休眠...");
// 睡眠3s。
TimeUnit.SECONDS.sleep(3);
log.debug("t1 休眠结束...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1");
// 1.启动线程。
t1.start();
log.debug("当前t1线程状态:{}", t1.getState());
// 当前t1线程状态:RUNNABLE
// 2.睡眠50ms。
TimeUnit.MILLISECONDS.sleep(50);
log.debug("当前t1线程状态:{}", t1.getState());
// 当前t1线程状态:TIMED_WAITING
// 3.等待线程结束。
t1.join();
}
sleep
会让当前线程从 Running
进入 Timed Waiting
状态(阻塞)。interrupt
方法打断正在睡眠的线程,这时 sleep
方法会抛出 InterruptedException
。TimeUnit
的 sleep()
代替 Thread
的 sleep()
来获得更好的可读性。join()
?首先我们来看一个案例。@Slf4j
public class JoinSample {
/**
* 成员变量初始值为0。
*/
private int num = 0;
@Test
public void test01() {
Thread t1 = new Thread(
() -> {
try {
TimeUnit.SECONDS.sleep(1);
// 为成员变量重新赋值。
num = 10;
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1");
t1.start();
log.debug("num={}", num);
// [main] DEBUG JoinSample - num=0
}
}
思考:此处为什么 num
得到的值是 0 而不是 10 ?
分析结论:主线程与子线程 t1 是并行执行的,t1 线程需要等待 1 秒后才能完成赋值操作,而主线程的执行速度快过子线程,所以日志打印结果 num=0
。
解决方法(使用join):
@Slf4j
public class JoinSample {
/**
* 成员变量初始值为0。
*/
private int num = 0;
@Test
public void test01() throws InterruptedException {
Thread t1 = new Thread(
() -> {
try {
TimeUnit.SECONDS.sleep(1);
// 为成员变量重新赋值。
num = 10;
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1");
t1.start();
// 1.使用join等待子线程t1执行结束。
t1.join();
log.debug("num={}", num);
// [main] DEBUG JoinSample - num=10
}
}
+------------+
| main | -+
+------------+ |
| |
| |
v |
+------------+ |
| t1.start() | |
+------------+ |
| |
| 1s |
v |
+------------+ |
| r=10 | |
+------------+ |
| |
| t1 end |
v |
+------------+ |
| t1.join | <+
+------------+
/**
* 打断 sleep,wait,join 的线程。
* 这几个方法都会让线程进入阻塞状态,此处以打断 sleep 的线程为例。
* 打断后会清空打断状态。
*
* @throws InterruptedException 中断异常
*/
@Test
public void test01() throws InterruptedException {
Thread t1 = new Thread(
() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1");
t1.start();
TimeUnit.MILLISECONDS.sleep(100);
t1.interrupt();
log.debug("t1 打断状态:{}", t1.isInterrupted());
// java.lang.InterruptedException: sleep interrupted ...
// [main] DEBUG InterruptSample - t1 打断状态:false
}
/**
* 打断正常运行的线程。
* 不会清空打断状态。
*
* @throws InterruptedException 中断异常
*/
@Test
public void test02() throws InterruptedException {
Thread t2 = new Thread(
() -> {
while (true) {
boolean interrupted = Thread.currentThread().isInterrupted();
if (interrupted) {
log.debug("t2 打断状态:{}", interrupted);
// [t2] DEBUG InterruptSample - t2 打断状态:true
break;
}
}
}, "t2");
t2.start();
t2.interrupt();
t2.join();
}
/**
* 打断 park 线程。
* 如果打断标记已经是 true, 则 park 会失效。
* 可以使用 Thread.interrupted() 清除打断状态。
*
* @throws InterruptedException 中断异常
*/
@Test
public void test03() throws InterruptedException {
Thread t3 = new Thread(
() -> {
log.debug("park...");
LockSupport.park();
log.debug("unpark...");
log.debug("t3 打断状态:{}", Thread.currentThread().isInterrupted());
// [t3] DEBUG InterruptSample - t3 打断状态:true
}, "t3");
t3.start();
t3.interrupt();
t3.join();
}
在一个线程 t1 中如何“优雅”终止线程 t2?
错误思路1:使用线程对象的 stop()
方法停止线程。
stop
方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其它线程将永远无法获取锁。错误思路2:使用 System.exit(int)
方法停止线程。
正确的做法是:执行完终止处理,再终止线程,即 Two-phase Termination,两阶段终止模式。
示意图:
isInterrupted()
实现两阶终止。 static class TPTInterrupt {
/**
* 线程。
*/
private Thread thread;
/**
* 利用 isInterrupted 实现两阶终止。
*/
public void start() {
thread = new Thread(
() -> {
while (true) {
// 1.当前线程是否被打断。
Thread currentThread = Thread.currentThread();
if (currentThread.isInterrupted()) {
// 若被打断标记,则执行收尾工作。
log.debug(" finish work ");
break;
}
try {
// 2.休眠 2 s。
TimeUnit.SECONDS.sleep(2);
log.debug(" save result ");
} catch (InterruptedException e) {
// 异常则设置打断标记。
currentThread.interrupt();
}
// 执行监控记录。
}
}, "monit-thread");
thread.start();
}
/**
* 停止线程方法。
*/
public void stop() {
thread.interrupt();
}
public static void main(String[] args) throws InterruptedException {
TPTInterrupt t = new TPTInterrupt();
t.start();
TimeUnit.SECONDS.sleep(3);
log.debug("stop");
t.stop();
// [monit-thread] DEBUG Sample - save result
// [main] DEBUG Sample - stop
// [monit-thread] DEBUG Sample - finish work
}
}
static class TPTVolatile {
/**
* 线程。
*/
private Thread thread;
/**
* 停止标记用 volatile 是为了保证该变量在多个线程之间的可见性。
* 即主线程把它修改为 true 对子线程可见。
*/
private volatile boolean stop = false;
public void start() {
thread = new Thread(
() -> {
while (true) {
// 1.当前线程是否被打断。
if (stop) {
// 若被打断标记,则执行收尾工作。
log.debug(" finish work ");
break;
}
try {
// 2.休眠 2 s。
TimeUnit.SECONDS.sleep(2);
log.debug(" save result ");
} catch (InterruptedException e) {
e.printStackTrace();
}
// 执行监控记录。
}
}, "monit-thread");
thread.start();
}
/**
* 停止线程方法。
*/
public void stop() {
// 重新设置标记。
stop = true;
thread.interrupt();
}
public static void main(String[] args) throws InterruptedException {
TPTVolatile t = new TPTVolatile();
t.start();
TimeUnit.SECONDS.sleep(3);
log.debug("stop");
t.stop();
// [monit-thread] DEBUG Sample - save result
// [main] DEBUG Sample - stop
// [monit-thread] DEBUG Sample - finish work
// java.lang.InterruptedException: sleep interrupted
}
}
方法名 | 是否为静态方法 | 功能说明 |
---|---|---|
stop() | 停止线程运行。 | |
suspend() | 挂起(暂停)线程运行。 | |
resume() | 恢复线程运行。 |
Java
进程需要等待所有线程都运行结束,才会结束。 /**
* [非守护线程]执行结束后,[守护线程]即使没有执行完也会强制结束。
*
* @throws InterruptedException 中断异常
*/
@Test
public void test01() throws InterruptedException {
Thread t1 = new Thread(
() -> {
try {
log.debug(" daemon start...");
TimeUnit.SECONDS.sleep(5);
log.debug(" daemon end...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "daemon");
t1.setDaemon(true);
t1.start();
TimeUnit.SECONDS.sleep(1);
log.debug(" main end ");
// [daemon] DEBUG Sample - daemon start...
// [main] DEBUG Sample - main end
}
Tomcat
中的 Acceptor
和 Poller
线程都是守护线程,所以 Tomcat
接收到 shutdown
命令后,不会等待它们处理完当前请求。cpu
调度执行。cpu
时间片运行中的状态,当 cpu
时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换。API
,如 BIO
读写文件,这时该线程实际不会用到 cpu
,会导致线程上下文切换,进入【阻塞状态】。BIO
操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】。根据 Thread.State 枚举,分为六种状态。
NEW
:线程刚被创建,但是还没有调用 start()
方法。RUNNABLE
:当调用了 start() 方法之后。注意:Java API 层面的 RUNNABLE
状态涵盖了操作系统层面的【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO
导致的线程阻塞,在 Java
里无法区分,仍然认为是可运行)。BLOCKED
, WAITING
, TIMED_WAITING
都是 Java API 层面对【阻塞状态】的细分。TERMINATED
当线程代码运行结束。“-------怕什么真理无穷,进一寸有一寸的欢喜。”
微信公众号搜索:饺子泡牛奶。