如何正确停止线程

新建线程

新建线程的方式有多种
  • 实现Runnable接口,重写run()方法
  • 继承Thread类,重写run()方法
  • 实现callable接口,重写call方法
  • 使用线程池
  • 使用定时器
  • …………..
代码举例-Runnable接口

Runnable接口重写run方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class NewRunnable implements Runnable{
@Override
public void run() {
System.out.println("I am NewRunnable Thread");
}

public static void main(String[] args) {
NewRunnable newRunnable = new NewRunnable();
Thread thread = new Thread(newRunnable);
thread.start();
}
}

// 结果
/*
I am NewRunnable Thread
*/

上述代码中新建NewRunnable 类实现了Runnable接口后重写了run方法,实现新建一个线程,可能这样体现不出来多线程,可以加个循环。

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
public class NewThread  {

public static void main(String[] args) {

Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(200);
System.out.println("i am "+ i);
} catch (InterruptedException e) {
e.printStackTrace();
}

}
}
});
thread.start();
try {
Thread.sleep(500);
System.out.println("i am mainThread");
} catch (InterruptedException e) {
e.printStackTrace();
}
}

运行结果

1
2
3
4
5
6
i am 0
i am 1
i am mainThread
i am 2
i am 3
i am 4

使用了匿名内部类重写了Runnable接口run方法,然后循环五次,每次睡眠200毫秒,主线程中睡眠500毫秒,所以当子线程循环两次后,在第三次打印中间主线程开始打印。

代码举例-Thread类

继承Thread类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class NewThread extends Thread {

@Override
public void run() {
System.out.println("I am NewThread!");
}

public static void main(String[] args) {
NewThread newThread = new NewThread();
newThread.start();
}
}
/*
I am NewThread!
*/
新建线程总结

无论是实现Runnable接口还是继承Thread类,发现最终都是通过Thread类的start()方法开启的线程,而不是通过run()方法,看一下run方法和start方法的源代码

run()方法
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Thread implements Runnable {

public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}

@Override
public void run() {
if (target != null) {
target.run();
}
}
}

发现Thread也实现了Runnable接口,并重写了run方法,如上所示,我们在使用实现Runnable接口,并把对象当作参数传给Thread类中 public Thread(Runnable target);构造器时,这时候run()方法执行的就是传入对象的run()方法,run()方法中也只有这三行代码,所以并没有实现多线程。

start()方法
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
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();

/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);

boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}

可以看到首先run()方法if (threadStatus != 0) 判断线程状态,而后调用了start0方法,而start0方法private native void start0();可以看到是一个native方法,并非是由java实现

搜索后得知

1
2
3
在Java中,Thread类是用于实现多线程编程的类。当我们创建一个新线程并启动它时,实际上是调用了Thread类中的start()方法,该方法会执行一个本地方法start0()。start0()方法会启动一个新的系统线程,并调用run()方法。run()方法是线程执行的代码,当run()方法执行完毕后,线程就会自动结束。

start0()方法是一个本地方法(native method),它是由JVM实现的。本地方法是指使用C/C++等低级语言编写的方法,它们通常由JVM加载并在本地系统上执行。start0()方法在底层实现了线程的创建和启动,其具体实现会依赖于不同的操作系统和JVM实现。在JVM中,start0()方法的实现是由native层面提供的,因此我们无法直接查看其源代码。

所以我们可以得知,在java中真正实现多线程的是Thread类中的start()方法。

补充 callable接口
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
public class NewCallable implements Callable {
@Override
public String call() throws Exception {
String s = "I am newCallable!";
System.out.println(Thread.currentThread().getName());

return s;
}

public static void main(String[] args) {
NewCallable newCallable = new NewCallable();

FutureTask futureTask = new FutureTask<>(newCallable);
Thread thread = new Thread(futureTask);
thread.start();
try {
Object o = futureTask.get();
System.out.println(o);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println("i am mainThread");
System.out.println(Thread.currentThread().getName());


}
}

/*
Thread-0
I am newCallable!
i am mainThread
main
*/

可以看到实现callable接口也是通过Thread类创建的新线程,不同的是使用了FutureTask类接收了返回值

简要说一下futureTask可以看到该类间接继承了Runnable接口,所以依然可以理解成callable接口就是实现Runnbale接口,重写了run方法的方式实现了多线程,其他方式不在过多阐释。

停止线程

如何停止线程?
  • stop()?
  • destroy()?
  • suspend()?
  • interrupt()?

查看 API,我们会看到 java.lang.Thread 类型提供了一系列的方法如 start()、stop()、resume()、suspend()、destroy()等方法来管理线程。但是除了 start() 之外,其它几个方法都被声名为已过时(deprecated)

详细原因可看jdk文档的解释

1
https://docs.oracle.com/javase/1.5.0/docs/guide/misc/threadPrimitiveDeprecation.html

正确停止线程的方式是使用interrupt()方法

Interrupt()方法

在 Java 中,Thread.interrupt() 方法用于中断线程,其作用是设置线程的中断标志位为 true。被中断的线程可以通过检查自身的中断标志位来判断是否被中断,然后执行适当的操作。

如果线程被阻塞在某些操作上(如等待 I/O 操作、sleep() 等),调用 interrupt() 方法会中断该线程的阻塞状态,抛出 InterruptedException 异常,并清除中断标志位。如果线程没有被阻塞,则调用 interrupt() 方法只是设置线程的中断标志位为 true,线程仍然可以继续运行。

举个例子
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
public class MyThread extends Thread {
public void run() {
try {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("MyThread is running...");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println("MyThread is interrupted!");
}
}

public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
myThread.interrupt();
}
}

// 结果
/*
MyThread is running...
MyThread is running...
MyThread is running...
MyThread is running...
MyThread is running...
MyThread is interrupted!
*/

新建了一个线程,在主线程中,我们睡眠5S后将新建线程中断,这是新线程捕获中断,并打印了MyThread is interrupted!

中断标志位

上方代码中,如果我们把while (!Thread.currentThread().isInterrupted()) 换成 while (true)发现运行结果是一样的,为什么呢?这就牵扯到中断标志位了,如上方所言 如果线程被阻塞在某些操作上(如等待 I/O 操作、sleep() 等),调用 interrupt() 方法会中断该线程的阻塞状态,抛出 InterruptedException 异常,并清除中断标志位。上方代码中5s中断线程时,线程在sleep状态,这时候程序抛出 InterruptedException 异常,并清除中断标志位。在抛出异常时线程已经停止了。

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
public class MyThread extends Thread {
public void run() {
int i =0;
while (!Thread.currentThread().isInterrupted()){
System.out.println("正在打印:"+ ++i);
}

}

public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
int sum = 0;
for (int i = 0; i < 1000000; i++) {
sum += i;
}
myThread.interrupt();
}
}


// 结果
/*
正在打印:0
正在打印:1
正在打印:2
正在打印:3
........
正在打印:104
*/

可以看到我们通过判断程序的中断信号来执行代码,当程序被中断时,跳出while循环,新线程运行结束

循环中使用注意事项

如果我们将try catch 放在循环中发现程序响应中断后并未停止,这是因为sleep()清除了中断标志位,所以不能将try catch语句放在循环中,而应该放在循环外。

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 MyThread extends Thread {
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
System.out.println("MyThread is running...");
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
myThread.interrupt();
}
}

结果

1
2
3
4
5
6
7
8
9
10
11
/*
MyThread is running...
MyThread is running...
MyThread is running...
MyThread is running...
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at threadToStop.MyThread.run(MyThread.java:8)
MyThread is running...
MyThread is running...
*/