进行Java并发程序设计的第一步就是要了解Java中为线程操作提供的一些API。比如,如何创建并运行线程,如何中断线程,如何终止线程等。因为并行操作要比串行操作复杂的多,所以在日常使用中我们经常会遇到一些意想不到的情况,本章将尽可能将一些潜在问题描述清楚。
1.新建线程
新建线程的操作很简单,只需要new一个线程对象并调用start()方法。
Thread t = new Thread();
t.start();
当Thread对象调用start()方法后,会新开启一个线程来执行对象的run()方法。需要注意的是,直接调用run方法并不会开启新线程,而是会在当前线程串行执行。
Thread t = new Thread();
/**
* run方法只会在当前线程串行执行
*/
t.run();
默认情况下,run()方法什么都不会做,如果想在线程生命周期内执行任务,就需要重载run()方法。
Thread t = new Thread(){
@Override
public void run() {
System.out.println("Hello, I'm Thread-1");
}
};
t.start();
上述代码使用匿名内部类重载了run()方法来定义线程需要执行的任务。但考虑到Java是单继承的,也就是说继承本身也是一种很宝贵的资源,因此Java提供了另一种方法——Runnable接口——来实现同样的操作,并且不需要继承Thread类。Runnable是一个单方法的接口,只有一个run()方法:
public interface Runnable {
public void run();
}
在Thread中有一个非常重要的构造器方法:
public Thread(Runnable target)
它传入一个Runnable实例,而在Thread.run()方法中,默认正是调用了Runnable.run()方法。
public void run() {
if (target != null) {
target.run();
}
}
使用Runnable构造Thread的代码示例:
public class CreateThreadWithRunnable implements Runnable {
public static void main(String[] args) {
Thread t = new Thread(new CreateThreadWithRunnable());
t.start();
}
public void run() {
System.out.println("Hello, I'm a Runnable");
}
}
2.终止线程
一般来说,线程执行完毕后就会结束,无需手动关闭。但在某些情况下(比如服务端的后台线程可能会常驻系统)线程的任务是一个循环,用于提供某些服务。
那如何关闭一个线程呢?不难发现,Thread类中提供了一个stop()方法,可以将一个线程立即终止。但现在这个方法已经被废弃了。也就是说,JDK在将来可能会移除这个方法。
为什么stop()被废弃而不推荐使用呢?原因是stop()方法过于暴力,强行把执行中的线程终止,可能会引起数据不一致的问题。
public class StopThreadUnsafe {
private static User user = new User();
public static class User {
private int id;
private String name;
public User() {
id = 0;
name = "0";
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public static class ChangeObjectThread extends Thread {
@Override
public void run() {
while (true) {
synchronized (user) {
int v = (int)(System.currentTimeMillis() / 1000);
user.setId(v);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
user.setName(String.valueOf(v));
}
Thread.yield();
}
}
}
public static class ReadObjectThread extends Thread {
@Override
public void run() {
while (true) {
synchronized (user) {
if (user.getId() != Integer.parseInt(user.getName())) {
System.out.println(user);
}
}
Thread.yield();
}
}
}
public static void main(String[] args) throws InterruptedException {
new ReadObjectThread().start();
while (true) {
ChangeObjectThread t = new ChangeObjectThread();
t.start();
Thread.sleep(150);
t.stop();
}
}
}
执行以上代码,很容易就会得到类似如下输出,id和name产生了不一致。
User{id=1575441092, name='1575441091'}
User{id=1575441096, name='1575441095'}
这类问题一旦出现在生产环境中将很难排查,因为不会抛出异常。这种情况一旦混杂在动辄几万十几万行的项目中,想要发现它们就只能凭借时间、经验和一点运气了。因此除非不得已,否则不要随便使用stop()方法来停止一个线程。
接下来演示一下如何安全的停止一个线程,如下代码所示:
public static class ChangeObjectThread extends Thread {
private volatile boolean stopped = false;
public void doStop() {
stopped = true;
}
@Override
public void run() {
while (true) {
if (stopped) {
System.out.println("Thread has been stopped, exit");
break;
}
synchronized (user) {
int v = (int)(System.currentTimeMillis() / 1000);
user.setId(v);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
user.setName(String.valueOf(v));
}
Thread.yield();
}
}
}
代码中添加了一个doStop()方法,执行后将stopped字段设置为true,循环中检查到这个改动后,线程就会跳出循环从而执行完毕,线程结束。这样线程就免于在修改user对象时被强制退出,也就避免了user对象状态的不一致。
3.线程中断
线程中断是一种非常重要的线程协作机制。从表面上理解,中断就是让线程停止执行的意思,实际上并非完全如此。严格地讲,线程中断并不会使线程立刻退出,而是相当于给线程发送一个希望它退出的通知。至于目标线程接到通知后如何处理,则由目标线程自行决定。 与线程中断有关的有三个方法,这三个方法看起来很相似,所以需要说明一下。
public void interrupt() // 中断线程
public void isInterrupted() // 判断线程是否被中断
public static void interrupted() // 判断线程是否被中断,并清除当前中断状态
- Thread.interrupt()方法是一个实例方法,它用于通知目标线程中断,即设置中断标志位。中断标志位表示当前线程已被中断。
- Thread.isInterrupted()方法也是实例方法,通过检查中断标志位来判断当前线程是否已被中断。
- 最后的静态方法Thread.interrupted()也是用来判断线程是否中断,但会同时清除当前线程的中断标志位状态。
下面这段代码对线程t进行了中断,但t并不会停止执行。
public static void main(String[] args) throw InterruptedException {
Thread t = new Thread(){
@Override
public void run() {
while(true) {
Thread.yield();
}
}
};
t.start();
Thread.sleep(2000);
t.interrupt();
}
虽然对t进行了中断,但在t的run()方法中并没有处理中断的逻辑,因此即使t被设置为中断状态,但这个中断不会发生任何作用。
如果希望在中断后退出,就需要增加中断处理代码:
public static void main(String[] args) throw InterruptedException {
Thread t = new Thread(){
@Override
public void run() {
while(true) {
if (Thread.currentThread.isInterrupt()) {
System.out.println("Interrupted");
break;
}
Thread.yield();
}
}
};
t.start();
Thread.sleep(2000);
t.interrupt();
}
上述代码添加了使用Thread.isInterrupted()方法判断当前线程是否被中断,如果是则退出循环体,结束线程。这种方法看起来与前面自定义的doStop()方法类似,但实际上中断的功能更为强劲。比如在循环体中如果出现了sleep()或wait()操作,就只能通过中断来识别了。
Thread.sleep()方法会让当前线程休眠指定的一段时间,它会抛出一个InterruptedException。这个异常不是运行时异常,也就是说程序必须捕获并处理它,当线程在sleep()时被中断,就会产生这个异常。
public static void main(String[] args) throw InterruptedException {
Thread t = new Thread(){
@Override
public void run() {
while(true) {
if (Thread.currentThread.isInterrupt()) {
System.out.println("Interrupted");
break;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("Interrupted when sleep");
// 重新设置中断状态
Thread.currentThread.interrupt();
}
Thread.yield();
}
}
};
t.start();
Thread.sleep(2000);
t.interrupt();
}
上述代码中,如果线程在sleep()时被中断则会捕获InterruptedException异常,并且sleep()方法由于中断而抛出异常后会清除中断标记位。在catch子句中,由于已经捕获了异常,我们可以立即退出线程。但为了保护数据的完整性,必须进行后续的处理,因此执行了Thread.interrupt()再次设置中断标记,这样才能在下一次循环开始的中断检查中发现当前线程已被中断。