Java并发编程(五)——线程的基本操作(上)

229 阅读6分钟

进行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()再次设置中断标记,这样才能在下一次循环开始的中断检查中发现当前线程已被中断。