线程总结

什么是线程

维基百科中给到的解释

线程:是操作系统能够进行运算调度的最小单位,大部分情况下,它被包含在进程之中,是进程中的实际运作单位

线程区别于协程。线程是抢占式的,在单CPU单核的计算机上。一次性只能有一个线程处理任务,所谓的多线程,是多个线程相互抢占CPU处理自己的任务。

那为什么多线程能够提高运算能力?
维基百科这么形容

因为使用多线程技术,也可以把进程中负责I/O处理、人机交互而常被阻塞的部分与密集计算的部分分开来执行,从而提高了程序的执行效率

笔者理解,计算机处理一个任务。并不是一直不停的执行,而是在不停的调度,例如,在线程A空闲的时候,去执行线程B的任务,从而提高效率。

线程,进程,协程的关系

如上面解释什么是线程。线程是包含在进程之中。而协程更像是线程中的线程,协程相对于线程而言,更加轻量级。如果非要说他们之间的关系。笔者认为,就是没有关系。

协程本身就是一个很大的知识点。之后如果有时间,笔者会对协程进行知识总结。目前在看一本Kotlin协程这一本书。协程使用很简单,而理解它还是有点难的。笔者对协程的了解不是太清楚。还停留在使用阶段。。

如何启动线程

启动线程的方式有两种。

  • 继承Thread
  • 实现Runnable接口

实际使用中很少会继承Thread,只要有下面几个原因

  • JAVA是单继承。如果使用继承Thread的方式。就很大程度上限制了开发者去复用代码块。
  • Runnable接口可以多处复用。在JAVA中。接口没有单继承的限制,方便拓展和代码的复用

🌰举例说明

使用继承的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void testCustomThread(){
new MyThread().start();
}

public static class MyThread extends Thread{
@Override
public void run() {
super.run();
System.out.println(Thread.currentThread().getName() + " run...");

}
}

实现Runnable接口的方式,更容易做到拓展

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void testRun(){
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " run...");
}
};
Thread t1 = new Thread(runnable,"t1");
Thread t2 = new Thread(runnable,"t2");
t1.start();
t2.start();
}

Thread类中的start()和run()方法有什么区别?

先来一个面试题,请问下面代码。执行的是在哪个线程?

1
2
3
4
5
6
7
8
9
10
@Test
public void testRun(){
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " run...");
}
},"t1");
t.run();
}

恭喜回答主线程的同学,回答对了,没错。运行结果如下。是在主线程。

1
main run...

其实比较好理解。run()方法相当于你直接执行了代码。原本在哪个线程,还是在那个线程。等同于写了一个普通方法。而start()则是告诉计算机。线程已经准备好了,随时等着CPU调度,等CPU调度选中了这个线程。那么才会执行run()方法。此时在运行。结果很明显

1
t1 run...

线程有几种状态?

Thread源码中又分成了6种。

维基百科中又定义成了4种

其实这么回答都没错,只是选择的角度不同。所以认定的状态也不相同。

一般理解的5种状态

笔者认为下面的5种状态的描述,更容易能够认知到线程。

图片来源 线程5种状态及常见问题

创建状态

执行了new Thread(),创建线程,并且为其分配相应的内存空间和资源。此时并没有开始执行

就绪状态

执行了start()方法。进入线程队列排队。等待CPU调度

运行状态

被CPU调度选中,获取处理器资源。执行run方法的代码

阻塞状态

遇到人为挂起或者需要执行耗时的操作,让出CPU资源暂停执行。

  • 阻塞时,不在进入线程队列排队
  • 阻塞消除以后,进入就绪状态

终止状态

即死亡状态,run()任务执行完成,或者执行stop()destroy(),线程结束。

源码中的6种状态

在Java线程的源码中分成了6种状态.先看一张比较经典的图

再来看一下源码中对状态的枚举定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public enum State {
//新创建
NEW,
//可运行
RUNNABLE,
//被阻塞
BLOCKED,
//等待
WAITING,
//计时等待
TIMED_WAITING,
//被终止
TERMINATED;
}

不难发现。大体上和上面笔者认为比较好理解的5种状态差不多。只是多了等待计时等待。下来一起分析一下每个状态的含义。以及实例。

NEW

其中NEW可以理解成new Thread(),分配了内存和资源对应上面理解的创建状态,比较好理解

RUNNABLE

RUNNABLE状态可理解成上面的就绪状态运行状态。执行了start()方法。已经处于线程队列排队,并且可能已经被CPU调度选中,进入运行状态,在JVM层面统称为RUNNABLE可运行状态。

TERMINATED

对应的是上面的终止状态

🌰举例说明

下面代码就很好的演示了,线程t1的状态,刚开始的创建new Thread()状态是NEW,等待1S以后执行start(),t1的状态变成了RUNNABLE,执行完成以后等待1S,在看一下t1的状态就变成了TERMINATED

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void testNew() throws InterruptedException {
Thread thread = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " run...");
}, "t1");
System.out.println("线程name is " + thread.getName() + "; status is " + thread.getState().name());
Thread.sleep(1000);
thread.start();
System.out.println("线程name is " + thread.getName() + "; status is " + thread.getState().name());
Thread.sleep(1000);
System.out.println("线程name is " + thread.getName() + "; status is " + thread.getState().name());
}

最终执行的结果为

1
2
3
4
线程name is t1; status is NEW
线程name is t1; status is RUNNABLE
t1 run...
线程name is t1; status is TERMINATED

BLOCKED

阻塞状态BLOCKED,被synchronized块阻塞,例如在多线程中。线程A获取锁进入同步块,在其出来之前,如果线程B想进入,就会因为获取不到锁而阻塞在同步块之外,这时线程B的状态就是BLOCKED

🌰举例说明

下面代码有两个线程t1t2,线程t1先执行。执行任务5S,并且持有block对象的,等待1S,确保线程t1顺利执行。1S后,线程t2准备执行任务,却发现没有,此时的锁还在线程t1手上。并没释放。那么线程t2的状态就是BLOCKED

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
@Test
public void testBlocked() throws InterruptedException {
Object block = new Object();
Thread thread1 = new Thread(() -> {
synchronized (block){
System.out.println(Thread.currentThread().getName() + " run...");
try {
sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t1");
thread1.start();

Thread.sleep(1000);

Thread thread2 = new Thread(() -> {
synchronized (block){
System.out.println(Thread.currentThread().getName() + " run...");
}
}, "t2");
thread2.start();
System.out.println("线程name is " + thread2.getName() + "; status is " + thread2.getState().name());

}

执行结果为

1
2
t1 run...
线程name is t2; status is BLOCKED

WAITING

所谓的等待,是未满足特定条件下,线程执行了wait()操作,例如线程A需要满足存款大于200元才执行其他操作,如果未能满足存款大于200元的特定条件。执行了wait()操作,此时线程A进入WAITING等待状态,等待其他线程发出notify或者notifyAll的通知。线程A收到通知以后,会重新进入RUNNABLE状态。

🌰举例说明

下面代码线程t1先执行。发现存款少于200,执行了wait(),此时线程t的状态就是WAITING。线程t2给设置到了300元。并且触发了notify(),线程t1被重新调度以后,顺利执行。

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
@Test
public void testWaitIng() throws InterruptedException {

AtomicInteger data = new AtomicInteger(100);

Thread thread1 = new Thread(() -> {
synchronized (data){
while (data.get() <=200){
try {
System.out.println(Thread.currentThread().getName() + " wait...");
data.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " run...");
}

}, "t1");
thread1.start();

Thread.sleep(1000);

System.out.println("线程name is " + thread1.getName() + "; status is " + thread1.getState().name());

Thread thread2 = new Thread(() -> {
synchronized (data){
data.set(300);
System.out.println(Thread.currentThread().getName() + " notify...");
data.notifyAll();
}
}, "t2");
thread2.start();

}

运行结果

1
2
3
4
t1 wait...
线程name is t1; status is WAITING
t2 notify...
t1 run...

TIMED_WAITING

WAITING相似。区别在于使用wait(time),即超时时间多久。wait()则是无限等待。这里就不在写实例了,读者可以修改上面的wait方法即可。

join()有什么作用

join方法的注释上这么描述

Waits for this thread to die.

翻译过来就是等待线程死亡。意思是。只有等这个线程结束以后才会执行下面的代码。

🌰举例说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void testJoin() throws InterruptedException {
Thread thread = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " run...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " finish");
}, "t1");
thread.start();
//第一次执行 不添加join
//第二次执行 添加join
thread.join();
System.out.println(Thread.currentThread().getName() + " finish");

}

未添加join运行结果

1
2
main finish
t1 run...

未添加join()方法,程序都没有将t1 finish打印出来。因为对于main线程来说。他已经执行结束了。也不去关心t1线程是否执行完了。

添加join 运行结果

1
2
3
t1 run...
t1 finish
main finish

添加join()方法。main线程只能等着。因为t1线程还没执行完成。只有等t1线程执行完了。main线程才能继续执行。

sleep(),wait()的区别

  • sleep()是线程的方法。而wait()是Object的方法
  • sleep()和wait()都会释放cpu资源,而sleep()不会释放锁,wait()会释放锁

针对第二个差别,笔者这里可准备一个小的测试代码,请问下面代码 t1t2的状态是什么。

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
@Test
public void test() throws InterruptedException {
Object obj = new Object();
Thread t1 = new Thread(() -> {
synchronized (obj){
System.out.println(Thread.currentThread().getName() + " run...");
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " finish");
}
}, "t1");
t1.start();



Thread t2 = new Thread(() -> {
synchronized (obj){
System.out.println(Thread.currentThread().getName() + " run...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " finish");
}
}, "t2");
t2.start();

System.out.println(t1.getName() + " status:" + t1.getState().name());
System.out.println(t2.getName() + " status:" + t2.getState().name());
}

先看一下。笔者这里准备了两个线程,t1线程先执行,并且获取了,然后执行4S,这个时候会释放CPU资源。那么线程t2准备执行。但是因为现在的还在线程t1手上。所以。线程t1的状态应该是TIMED_WAITING等待状态。而线程t2因为获取不到锁而阻塞在同步块之外,其状态是BLOCKED运行结果如下,你答对了嘛

1
2
3
t1 run...
t1 status:TIMED_WAITING
t2 status:BLOCKED

如果此时将Thread.sleep(time);代码替换成obj.wait(time),请问 t1t2的状态是什么。

读者可以想一下。本小节的区别sleep()不会释放锁,wait()会释放锁,所以在t1wait的时候,t2开始执行。然后t1t2都等待着被唤醒。可惜啊。没人唤醒他们。运行结果如下。读者答对没有呢

1
2
3
4
t1 run...
t2 run...
t1 status:TIMED_WAITING
t2 status:TIMED_WAITING

yield()作用

源码中注释这么标注

A hint to the scheduler that the current thread is willing to yield

表示当前线程愿意让步。其本意是说,放弃cpu的资源。主动回归到等待队列中。让cpu进行调度。但是回归了队列,意味着依然有可能被重新选中。

🌰举例说明

准备了两个线程。t0t1,让他们打印0到4,并且在打印到第2个以后,yield()一下。按照我们的理解,他应该有两种可能。

  • 回归等待队列以后。又被重新选中了。prepare yield之后。继续执行当前线程的任务
  • 回归等待队列以后。没有重新,让步给了其他线程。prepare yield之后。执行其他线程的任务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void testYield() throws InterruptedException {
Runnable runnable = () -> {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " run " + i);
if (i == 2) {
System.out.println(Thread.currentThread().getName() + " prepare yield");
Thread.yield();
}
}
};
for (int i = 0; i < 2; i++) {
Thread thread = new Thread(runnable, "t" + i);
thread.start();
}
}

死锁是什么

百度百科中解释

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程

🌰举例说明

注意,之前的测试都是在测试代码中运行。这里是运行在main方法中,部分新的gralde版本在运行main方法会抛出错误。main not found,只需要在项目目录下的.idea/gradle.xml中添加<option name="delegatedBuild" value="false" />即可。

下面代码运行就发生死锁。线程t1锁住了obj1,线程t2锁住了obj2,就导致了死锁。

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
44
45
46
47
public static void main(String[] args) {
testDeadlock();
}

public static void testDeadlock() {
Object obj1 = new Object();
Object obj2 = new Object();
Thread thread1 = new Thread(() -> {
synchronized (obj1) {
System.out.println(Thread.currentThread().getName() + " 锁住obj1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj2){
System.out.println(Thread.currentThread().getName() + " 进不来的");
try {
Thread.sleep(1000) ;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "t1");

Thread thread2 = new Thread(() -> {
synchronized (obj2) {
System.out.println(Thread.currentThread().getName() + " 锁住obj2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj1){
System.out.println(Thread.currentThread().getName() + " 进不来的");
try {
Thread.sleep(1000) ;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "t2");
thread1.start();
thread2.start();
}

运行结果如下。注意看红框表示线程还在运行。并没有结束。

如何终止线程

正常退出

比较好理解。run()内的方法全部执行完成。线程正常结束。并且推出。就像在上面举例说明NEW–>RUNNABLE—>TERMINATED的过程。就是正常的退出

stop/destroy

Thread提供了 stopdestroy的方法用于退出程序。本质上他们都是抛出异常终止线程,可以看一下他们的源码。这种属于强行使用异常中断,并且方法被标记为废弃Deprecated.不建议使用

1
2
3
4
@Deprecated
public void destroy() {
throw new UnsupportedOperationException();
}
1
2
3
4
@Deprecated
public final void stop() {
throw new UnsupportedOperationException();
}

interrupt

本质上是通过标记。判断状态。会抛出java.lang.InterruptedException: sleep interrupted的异常。标记可以通过isInterrupted判断。下面演示一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void testInterrupt() throws InterruptedException {
Thread thread = new Thread(() -> {
if (!Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread().getName() + " run...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " finish");
} else {
System.out.println(Thread.currentThread().getName() + " interrupt");
}
}, "t1");
thread.start();
Thread.sleep(1000);
thread.interrupt();
}

运行结果

1
2
3
4
5
6
t1 run...
java.lang.InterruptedException: sleep interrupted
at java.base/java.lang.Thread.sleep(Native Method)
at com.allens.sample_thread.MainTest.lambda$testInterrupt$7(MainTest.java:125)
at java.base/java.lang.Thread.run(Thread.java:834)
t1 finish

参考