创建和运行线程
1、进程和线程
进程
- 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存。在 指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理IO的。
- 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
- 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器 等),也有的程序只能启动一个实例进程(例如网易云音乐、360安全卫士等)。
线程
- 一个进程之内可以分为一到多个线程。
- 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行。
- Java中,线程作为最小调度单位,进程作为资源分配的最小单位。在windows中进程是不活动的,只是作为线程的容器。
二者对比
- 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集。
- 进程拥有共享的资源,如内存空间等,供其内部的线程共享。
- 进程间通信较为复杂。
- 同一台计算机的进程通信称为
IPC(Inter-process communication)
。 - 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如HTTP。
- 同一台计算机的进程通信称为
- 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量。
- 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低。
2、并行与并发
单核cpu下,线程实际还是串行执行的。操作系统中有一个组件叫做任务调度器,将cpu的时间片(windows 下时间片最小约为15毫秒)分给不同的程序使用,只是由于cpu在线程间(时间片很短)的切换非常快,人类感觉是同时运行的 。总结为一句话就是:微观串行,宏观并行, 一般会将这种线程轮流使用CPU的做法称为并发(concurrent)。
多核cpu下,每个核(core)都可以调度运行线程,这时候线程可以是并行的。
引用编程大师Rob Pike的一段描述:
- 并发(concurrent)是同一时间应对(dealing with)多件事情的能力。
- 并行(parallel)是同一时间动手做(doing)多件事情的能力。
线程上下文切换(Thread Context Switch)
- 因为以下一些原因导致cpu不再执行当前的线程,转而执行另一个线程的代码:
- 线程的cpu时间片用完。
- 垃圾回收。
- 有更高优先级的线程需要运行。
- 线程自己调用了sleep、yield、wait、join、park、synchronized、lock等方法。
- 当Context Switch发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条jvm指令的执行地址,是线程私有的。
- 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等。
- Context Switch频繁发生会影响性能。
3、创建和使用线程
3.1 线程创建
3.1.1 直接使用Thread
模板
1
2
3
4
5
6
7
8// 创建线程对象
Thread t = new Thread() {
public void run() {
// 要执行的任务
}
};
// 启动线程
t.start();例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class TestThread {
public static void main(String[] args) {
// 构造方法的参数是给线程指定名字
Thread t = new Thread("t1"){
// run方法内实现了要执行的任务
public void run() {
log.info("Hello Thread");
}
};
t.start();
}
}
3.1.2 使用Runnable配合Thread
把线程和任务(要执行的代码)分开。
- Thread代表线程。
- Runnable可运行的任务(线程要执行的代码)。
模板
1
2
3
4
5
6
7
8
9Runnable runnable = new Runnable() {
public void run(){
// 要执行的任务
}
};
// 创建线程对象
Thread t = new Thread(runnable);
// 启动线程
t.start();例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TestThread {
public static void main(String[] args) {
// 创建任务对象
Runnable task = new Runnable() {
public void run() {
log.info("Hello Thread");
}
};
// 参数1是任务对象; 参数2是线程名字
Thread t = new Thread(task, "t");
t.start();
}
}Java8以后可以使用lambda精简代码:
1
2
3
4
5
6
7
8
9
10
11
12
public class TestThread {
public static void main(String[] args) {
// 创建任务对象
Runnable task = () -> {
log.info("Hello Thread");
};
// 参数1是任务对象; 参数2是线程名字
Thread t = new Thread(task, "t");
t.start();
}
}
Thread与Runnable的关系:
使用带有构造器参数的方式传入Runnable的实例时通过源码可以看到实际上最终这个对象会被赋值给Thread类里一个名为target,类型为Runnable的成员变量。并且在执行Thread.run()方法时会判断target是否为null,如果不为null才优先执行target.run()。
用Runnable更容易与线程池等高级API配合,让任务类脱离了Thread继承体系,更灵活。
3.1.3 FutureTask配合Thread
FutureTask能够接收Callable类型的参数,其间接实现了Runnable和Future接口,用来处理有返回结果的情况。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TestThread {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建任务对象
FutureTask<Integer> task = new FutureTask<>(() -> {
log.info("Hello Thread");
return 100;
});
// 参数1是任务对象; 参数2是线程名字
new Thread(task, "t").start();
// 主线程阻塞,同步等待task执行完毕的结果
Integer result = task.get();
log.debug("结果是:{}", result);
}
}
3.2 线程使用
3.2.1 start()与run()
调用run():程序仍在main线程运行,t1线程内的方法调用还是同步的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class TestThread {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Thread t1 = new Thread("t1") {
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("running...");
}
};
t1.run();
log.debug("do other things...");
}
}运行结果:
1
216:09:34.019 org.example.TestThread [main] - running...
16:09:34.019 org.example.TestThread [main] - do other things...调用start():程序在t1线程运行,t1线程内的方法调用是异步的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class TestThread {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Thread t1 = new Thread("t1") {
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("running...");
}
};
t1.start();
log.debug("do other things...");
}
}运行结果:
1
216:12:04.089 org.example.TestThread [main] - do other things...
16:12:07.089 org.example.TestThread [t1] - running...结论:
- 直接调用run是在主线程中执行了run,没有启动新的线程。
- 使用start是启动新的线程,通过新的线程间接执行run中的代码。
2.2 sleep()与yield()
调用sleep():会让当前线程从Running进入Timed Waiting状态(阻塞)。
- 睡眠结束后的线程未必会立刻得到执行。
- 建议用TimeUnit的sleep代替Thread的sleep来获得更好的可读性。(例如TimeUnit.SECONDS.sleep(1);)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class TestThread {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Thread t1 = new Thread("t1") {
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t1.start();
log.debug("t1 state: {}", t1.getState());// t1 state: RUNNABLE
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("t1 state: {}", t1.getState());// t1 state: TIMED_WAITING
}
}其它线程可以使用interrupt方法打断正在睡眠的线程,这时sleep方法会抛出InterruptedException。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class TestThread {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Thread t1 = new Thread("t1") {
public void run() {
log.debug("enter sleep...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
log.debug("wake up...");
e.printStackTrace();
}
}
};
t1.start();
Thread.sleep(1000);
log.debug("interrupt...");
t1.interrupt();
}
}运行结果:
1
2
3
4
5
616:22:22.726 org.example.TestThread [t1] - enter sleep...
16:22:23.735 org.example.TestThread [main] - interrupt...
16:22:23.735 org.example.TestThread [t1] - wake up...
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at org.example.TestThread$1.run(TestThread.java:23)
调用yield()
- 调用yield会让当前线程从Running进入Runnable就绪状态,然后调度执行其它线程。(让出去后还可能重新被分到CPU的时间片继续执行)
- 具体的实现依赖于操作系统的任务调度器。
2.3 join()
- t.join()方法只会使主线程(或者说调用t.join()的线程)进入等待池并等待t线程执行完毕后才会被唤醒。并不影响同一时刻处在运行状态的其他线程。
1 |
|
因为主线程和线程 t1 是并行执行的,t1 线程需要 1 秒之后才能算出 r=10。通过使用t1线程的join()使得主线程等待其结束并成功打印出修改后的值。运行结果:
1 | 20:59:03.203 org.example.TestThread [main] - 开始 |
有时效的join()规定最多等待的时间,如果超出所等待的时间就会放弃等待继续运行。如下方t1线程执行时间超过2秒,而主线程最多等待1.5秒,所以主线程没有等待t1线程完全执行结束(只等待了1.5秒)就接着继续运行了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class TestThread {
static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws ExecutionException, InterruptedException {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
r1 = 10;
});
long start = System.currentTimeMillis();
t1.start();
log.debug("join begin");
t1.join(1500);
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
}运行结果:
1
221:15:49.100 org.example.TestThread [main] - join begin
21:15:50.608 org.example.TestThread [main] - r1: 0 r2: 0 cost: 1508
2.4 interrupt()
如果线程被Object.wait,Thread.join和Thread.sleep三种方法之一阻塞,此时调用该线程的interrupt()方法,那么该线程将抛出一个InterruptedException中断异常(该线程必须事先预备好处理此异常),从而提早地终结被阻塞状态。调用interrupt()会立即将线程的中断标记设为“true”,但是由于线程处于阻塞状态,所以该“中断标记”会立即被清除为“false”,同时,会产生一个InterruptedException的异常。下面的isInterrupted()是线程的一个实例方法,作用是返回当前线程是否被打断,不会清除打断状态;与其区别的是Thread类的interrupted()方法是一个静态方法,作用是返回当前线程的被打断状态,同时清除打断状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class TestThread {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("sleep...");
try {
Thread.sleep(5000); // wait, join
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1");
t1.start();
// 主线程等待以便确保t1线程先进入睡眠状态
Thread.sleep(1000);
log.debug("interrupt");
t1.interrupt();
// 主线程等待以便t1线程抛出InterruptedException异常
Thread.sleep(500);
log.debug("打断标记:{}", t1.isInterrupted());// 标记被清空
}
}运行结果:
1
2
3
4
5
6
721:32:39.648 org.example.TestThread [t1] - sleep...
21:32:40.661 org.example.TestThread [main] - interrupt
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at org.example.TestThread.lambda$main$0(TestThread.java:21)
at java.lang.Thread.run(Thread.java:748)
21:32:41.174 org.example.TestThread [main] - 打断标记:false打断正常运行的线程打断正常运行的线程, 不会清空打断状态,此时被打断的线程可根据打断标记来决定自己是否要停止运行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class TestThread {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while(true) {
boolean interrupted = Thread.currentThread().isInterrupted();
if(interrupted) {
log.debug("被打断了, 退出循环");
break;
}
}
}, "t1");
t1.start();
Thread.sleep(1000);
log.debug("interrupt");
t1.interrupt();
}
}执行结果:
1
221:51:21.970 org.example.TestThread [main] - interrupt
21:51:21.972 org.example.TestThread [t1] - 被打断了, 退出循环打断park线程, 不会清空打断状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TestThread {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("park...");
LockSupport.park();
log.debug("unpark...");
log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
}, "t1");
t1.start();
Thread.sleep(1000);
t1.interrupt();
}
}运行结果:
1
2
310:40:39.969 org.example.TestThread [t1] - park...
10:40:40.969 org.example.TestThread [t1] - unpark...
10:40:40.969 org.example.TestThread [t1] - 打断状态:true打断状态为true后如果继续执行LockSupport.park()则失效,如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TestThread {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("park...");
LockSupport.park();
log.debug("unpark...");
log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
LockSupport.park();
log.debug("unpark...");
}, "t1");
t1.start();
Thread.sleep(1000);
t1.interrupt();
}
}运行结果:
1
2
3
410:41:31.009 org.example.TestThread [t1] - park...
10:41:32.010 org.example.TestThread [t1] - unpark...
10:41:32.010 org.example.TestThread [t1] - 打断状态:true
10:41:32.010 org.example.TestThread [t1] - unpark...在一个线程t1中“优雅”终止线程t2(即给线程t2一个料理后事的机会)的方式。
错误的方式:
- 使用线程对象的stop()方法停止线程:stop方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其它线程将永远无法获取锁。
- 使用System.exit(int)方法停止线程:目的仅是停止一个线程,但这种做法会让整个程序都停止。
以下图为例子:
此案例的监控线程被打断有两种情况,第一种情况是监控线程在正常监控时被打断,但此时打断标记不会被清空,在通过下一次while循环时即可结束循环;第二种情况是监控线程在睡眠时被打断,这时由于打断标记会被清除,所以要手动设置打断标记为true以便在下次while循环时能结束循环。
方式一:利用isInterrupted。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class MonitorThreadTest {
private Thread thread;
public static void main(String[] args) throws InterruptedException {
MonitorThreadTest monitorThreadTest = new MonitorThreadTest();
monitorThreadTest.start();
Thread.sleep(3500);
// 在监控线程睡眠过程中打断
monitorThreadTest.stop();
}
public void start(){
thread = new Thread(() -> {
while(true) {
Thread current = Thread.currentThread();
if(current.isInterrupted()) {
log.debug("料理后事");
break;
}
try {
// 情况一
Thread.sleep(1000);
// 情况二
log.debug("执行监控操作");
} catch (InterruptedException e) {
e.printStackTrace();
// 重新设置打断标记
current.interrupt();
}
}
},"监控线程");
thread.start();
}
public void stop() {
thread.interrupt();
}
}运行结果:
1
2
3
4
5
6
7
822:14:37.595 org.example.MonitorThreadTest [监控线程] - 执行监控操作
22:14:38.600 org.example.MonitorThreadTest [监控线程] - 执行监控操作
22:14:39.610 org.example.MonitorThreadTest [监控线程] - 执行监控操作
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at org.example.MonitorThreadTest.lambda$start$0(MonitorThreadTest.java:32)
at java.lang.Thread.run(Thread.java:748)
22:14:40.104 org.example.MonitorThreadTest [监控线程] - 料理后事方式二:利用打断标记。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class MonitorThreadTest {
private Thread thread;
// 停止标记
private volatile boolean stop = false;
public static void main(String[] args) throws InterruptedException {
MonitorThreadTest monitorThreadTest = new MonitorThreadTest();
monitorThreadTest.start();
Thread.sleep(3500);
// 在监控线程睡眠过程中打断
monitorThreadTest.stop();
}
public void start(){
thread = new Thread(() -> {
while (true) {
// 是否被打断
if (stop) {
log.debug("料理后事");
break;
}
try {
Thread.sleep(1000);
log.debug("执行监控记录");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"监控线程");
thread.start();
}
public void stop() {
stop = true;
thread.interrupt();
}
}运行结果:
1
2
3
4
5
6
7
822:27:10.648 org.example.MonitorThreadTest [监控线程] - 执行监控记录
22:27:11.667 org.example.MonitorThreadTest [监控线程] - 执行监控记录
22:27:12.667 org.example.MonitorThreadTest [监控线程] - 执行监控记录
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at org.example.MonitorThreadTest.lambda$start$0(MonitorThreadTest.java:33)
at java.lang.Thread.run(Thread.java:748)
22:27:13.139 org.example.MonitorThreadTest [监控线程] - 料理后事
2.5 wait()和notify()
操作系统的Monitor对象结构如下图:
- 已经成为Monitor的Owner如果发现条件不满足,可以调用wait方法,即可进入WaitSet变为WAITING状态。
- BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片。
- BLOCKED线程会在Owner线程释放锁时唤醒。
- WAITING线程会在Owner线程调用notify或notifyAll时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList重新竞争。
obj.wait()让进入object监视器的线程到waitSet等待。
- wait()方法会释放对象的锁,进入WaitSet等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到notify为止。
- wait(long n)是有时限的等待, 到n毫秒后结束等待,或是被notify。
obj.notify()在object上正在waitSet等待的线程中挑一个唤醒。
obj.notifyAll()让object上正在waitSet等待的线程全部唤醒。
它们都是线程之间进行协作的手段,都属于Object对象的方法。必须获得此对象的锁,才能调用这几个方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class TestWaitNotify {
final static Object obj = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
},"t1").start();
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
},"t2").start();
// 主线程两秒后执行
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("唤醒obj上其它线程");
synchronized (obj) {
// obj.notify(); // 唤醒obj上一个线程
obj.notifyAll(); // 唤醒obj上所有等待线程
}
}
}当调用obj.notify()时随机唤醒某一个在waitSet中等待的线程,执行结果:
obj.notifyAll()时唤醒所有在waitSet中等待的线程,执行结果:
sleep(long n)和wait(long n)的区别。
- sleep是Thread类的静态方法,而wait是Object的实例方法。
- sleep不需要强制和synchronized配合使用,但wait需要和synchronized一起用。
- sleep在睡眠的同时,不会释放对象锁的,但wait在等待的时候会释放对象锁。
- 它们状态都是TIMED_WAITING。
2.6 park()和unpark()
它们是LockSupport类中的方法。
1
2
3
4// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)先park再unpark:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class TestParkAndUnPark {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("start...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("park...");
LockSupport.park();
log.debug("resume...");
},"t1");
t1.start();
Thread.sleep(2000);
log.debug("unpark...");
LockSupport.unpark(t1);
}
}运行结果:
先unpark再park则可以恢复未来暂停进程的运行:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public class TestParkAndUnPark {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("start...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("park...");
LockSupport.park();
log.debug("resume...");
}, "t1");
t1.start();
Thread.sleep(1000);
log.debug("unpark...");
LockSupport.unpark(t1);
}
}运行结果:
与Object的wait¬ify相比:
- wait,notify和notifyAll必须配合Object Monitor一起使用,而park,unpark不必。
- park,unpark是以线程为单位来阻塞和唤醒线程,而notify只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么精确。
- park&unpark可以先unpark,而wait¬ify不能先notify。
原理。
每个线程都有自己的一个Parker对象,由三部分组成_counter ,_cond和_mutex,其中_mutex中包含等待队列_cond。
打个比喻:线程就像一个旅人,Parker就像他随身携带的背包,条件变量就好比背包中的帐篷。_counter 就好比背包中的备用干粮(0为耗尽,1为充足)。
- 调用park就是要看需不需要停下来歇息。
如果备用干粮耗尽,那么钻进帐篷歇息。
+ 如果备用干粮充足,那么不需停留,继续前进。调用unpark,就好比令干粮充足。
- 如果这时线程还在帐篷,就唤醒让他继续前进。
- 如果这时线程还在运行,那么下次他调用park时,仅是消耗掉备用干粮,不需停留继续前进。
- 因为背包空间有限,多次调用unpark仅会补充一份备用干粮。
- ①当前线程调用Unsafe.park()方法。
②检查_counter,本情况为0,这时,获得_mutex互斥锁。
- ③线程进入_cond条件变量阻塞。
④设置_counter=0。
- ①调用Unsafe.unpark(Thread_0)方法,设置_counter为1。
- ②唤醒_cond条件变量中的Thread_0。
- ③Thread_0恢复运行。
- ④设置_counter为0。
- ①调用Unsafe.unpark(Thread_0)方法,设置_counter为1。
- ②当前线程调用Unsafe.park()方法。
- ③检查_counter,本情况为1,这时线程无需阻塞,继续运行。
- ④设置_counter为0。