concurrency_thread

第二十二章 Java线程的创建和销毁

Java语言提供两种方式创建一个新的线程。第一种方式是实现Runnable接口;另一种方式是继承Thread类。

1 Runnable接口

Runnable接口是Java语言提供的一种通用的线程创建机制。任何实现了Runnable接口的类都能在新的线程中执行。Runnable接口只提供了一个方法run();继承类需要实现这个run()方法。新线程会运行run()方法中的代码。

有以下三种方法创建Runnable对象。

1.1 创建一个新类,实现Runnable接口

第一种方法是最为常见的一种方法。开发人员可以自定义一个新类,并实现Runnable接口。例如:下面的ThreadExample类实现了Runnable接口,在run()方法中打印一行字符"This is running in a new thread"。

public class ThreadExample implements Runnable {
    @Override
    public void run() {
        System.out.println("This is running in a new thread");
    }

    public static void main(String[] args) {
        Runnable newClass = new ThreadExample();
        Thread thread = new Thread(newClass);
        
        // 启动新线程
        thread.start();
    }
}

程序运行结果:

> java ThreadExample
This is running in a new thread

1.2 使用匿名类,实现Runnable接口

在一些场景下,少定义一个新类有利于简化代码,降低开发人员的工作量。所以,在此时,使用匿名类是一个不错的选择。在如下的例子中,并没有定义一个全新的类来表示新线程的运行逻辑。Java虚拟机会为其自动创建一个匿名类。

public class AnonymousThreadExample {
    public static void main(String[] args) {
        // 创建一个匿名类,实现Runnable接口
        Runnable anonymousThread = new Runnable() {
            public void run() {
                System.out.println("This is running in a new thread");
            }
        };

        Thread thread = new Thread(anonymousThread);
        
        // 启动新线程
        thread.start();
    }
}

程序运行结果:

> java AnonymousThreadExample
This is running in a new thread

1.3 使用Lambda表达式实现Runnable接口

如果还想省略更多的代码,那么,可以使用Lambda表达式来创建一个Runnable接口的对象。因为Runnable接口只提供了一个方法run()。因此,Runnable接口也是一个Functional Interface。所以,Lambda表达式也可以转换成Runnable接口的对象。(注:Java 8以上版本才支持Lambda表达式和Functional Interface。)

public class LambdaExpressionThreadExample {
    public static void main(String[] args) {
        // 使用Lambda表达式创建一个Runnable对象
        Runnable lambdaExpressionThread = 
            () -> {System.out.println("This is running in a new thread");};
        
        Thread thread = new Thread(lambdaExpressionThread);
        
        // 启动新线程
        thread.start();
    }
}

程序运行结果:

> java LambdaExpressionThreadExample
This is running in a new thread

2 Thread类

Thread类用于表达一个线程对象。它实现了多种方法,用于启动/停止线程,设置/获取线程信息等功能。Thread类也实现了Runnable接口。因此,继承自Thread类的子类,并覆盖run()方法的话,也能运行于新的线程中。例如:

public class ConcreteThreadExample extends Thread {
    @Override
    public void run() {
        System.out.println("This is running in a new thread");
    }
    
    public static void main(String[] args) {
        Thread newThread = new ConcreteThreadExample();
        newThread.start();
    }
}

程序运行结果:

> java ConcreteThreadExample
This is running in a new thread

2.1 启动线程

无论是通过实现Runnable接口或者继承Thread类创建新线程,都是调用Thread.start()方法来启动线程的。新线程会自动运行run()方法。当新线程启动后,start()方法会立刻返回;此刻,主线程和新线程将同时运行。

2.2 停止/打断线程

当需要停止/打断一个线程时,开发人员可以调用Thread.interrupt()方法。interrupt()方法的意义是通知线程,现在需要“放下手上的活,去做其他的一些事情”。这里的“其他的事情”可以是去处理一些其他的任务,也可以是直接退出。当线程被打断(interrupted),且该线程正处于等待状态时,Java虚拟机会在该线程中抛出InterruptedException或者ClosedByInterruptException异常(详细内容请参见Java文档)。如果该线程是在运行状态,则Java虚拟机会设置被打扰的标志。当线程调用Thread.isInterrupted()方法时,该方法会返回true。例如:

public class ConcreteThreadExample extends Thread {
    @Override
    public void run() {
        try {
            Thread.sleep(10000); //等待10秒
        } catch (InterruptedException ex) {
            System.out.println("Interrupted. Exit.");
            return;  // 线程被打断,退出
        } catch (IllegalArgumentException ex) {
        }
    }

    public static void main(String[] args) {
        Thread newThread = new ConcreteThreadExample();
        newThread.start();
        
        try {
            // 主线程先等待1秒
            System.out.println("The main thread sleeps.");
            Thread.sleep(1000);
            System.out.println("The main thread wakes up.");
        
            // 然后再打断线程newThread
            newThread.interrupt();
        } catch (Exception ex) {}
    }
}

程序运行结果:

> java ConcreteThreadExample
The main thread sleeps.
The main thread wakes up.
Interrupted. Exit.

2.3 线程优先级(Priority)

每个Java线程都有优先级。优先级高的线程会被优先执行。Java线程优先级的范围是[1, 10]。优先级10最高,1最低。线程的默认优先级是5。

在Thread类中还提供了三个优先级常量,供开发人员使用。它们是:Thread.MIN_PRIORITY, Thread.NORM_PRIORITY, Thread.MAX_PRIORITY。

Thread类还提供了方法getPriority()和setPriority()用于获取和设置线程优先级。例如:

public class ConcreteThreadExample extends Thread {
    public static void main(String[] args) {
        // 使用Lambda表达式创建一个Runnable对象
        Runnable codes = ()->{};

        // 创建一个新线程,并设置其为最低优先级线程
        Thread newThread = new Thread(codes);
        newThread.setPriority(Thread.MIN_PRIORITY);
        System.out.println("The priority of newThread is " + newThread.getPriority());
        newThread.start();
    }
}

程序运行结果:

> java ConcreteThreadExample
The priority of newThread is 1

2.4 等待线程

Thread类的方法join()用于等待一个线程执行完毕。当一个线程使用join()方法等待另一个线程时,该线程会停止执行,直到等待的线程被销毁(正常或者异常退出)。这意味着,调用join()方法会阻塞当前线程的运行。例如:

public class JoinThreadExample extends Thread {
    public static void main(String[] args) {
        // 使用Lambda表达式创建一个Runnable对象,计算一万以内整数的和
        Runnable codes = ()->{
            int sum = 0;
            for (int i = 0; i < 10000; i++) {
                sum += i;
            }
            System.out.println("sum is " + sum);
        };

        // 创建一个新线程,并设置其为最低优先级线程
        Thread newThread = new Thread(codes);
        newThread.start();

        try {
            // main函数被阻塞在这条语句,直至newThread执行完毕
            newThread.join();
            System.out.println("newThread completes.");
        } catch (Exception ex) {}
    }
}

程序运行结果:

> java JoinThreadExample
sum is 49995000
newThread completes.

2.5 yield(让出CPU)

Thread类的方法yield()可以向Java虚拟机传递一个信号:当前线程自愿让出CPU,以使得其他线程获得执行的机会。在某些情况下,yield()方法非常有用。例如,在调式时,需要暂停一个线程的执行,以创造出竞态条件(Race Condition)。但是,值得注意的是,yield()方法只是表达一种意愿。实际上,CPU资源是否让出是由Java虚拟机根据多方因素综合决定的。

public class ConcreteThreadExample extends Thread {
    @Override
    public void run() {
        try {
            // 计算一万以内的整数和
            int sum = 0;
            for(int i = 0; i < 10000; i++) {
                sum += i;

                // 每循环1000次,让出CPU一次
                if (i % 1000 == 0) {
                    Thread.yield();
                }
            }
        } catch (Exception ex) {} 
    }

    public static void main(String[] args) {
        Thread newThread = new ConcreteThreadExample();
        newThread.start();
    }
}

2.6 Daemon线程(守护线程)

Daemon线程即守护线程。Daemon线程与非Daemon线程的区别是:当Java虚拟机中只有Daemon线程在运行时,Java虚拟机会退出。换句话说,如果当所有非Daemon线程执行完毕后,Java虚拟机会退出。运行main方法的线程为非Daemon线程。一个典型的Daemon线程是垃圾回收线程(Garbage Collection Thread)。

在创建新的线程时,Thread.setDaemon()方法为开发人员提供了将线程设置为Daemon线程的机会。开发人员还可以通过使用Thread.isDaemon()判断一个线程是否为Daemon线程。例如:

public class DaemonThreadExample extends Thread {
    public static void main(String[] args) {
        // 使用Lambda表达式创建一个Runnable对象
        Runnable codes = ()->{};

        // 创建一个新线程,并设置其为最低优先级线程
        Thread newThread = new Thread(codes);
        newThread.setDaemon(true);
        newThread.start();
        System.out.println("Is newThread a daemon thread? " + newThread.isDaemon());
    }
}

程序运行结果:

> java DaemonThreadExample
Is newThread a daemon thread? true

2.7 查看线程信息

Thread类还提供了一些查看线程状态的方法。这些方法总结如下:

方法名称意义
Thread::getId()返回该线程的标识(id)。
Thread::getStackTrace()返回当前线程的调用栈信息。
Thread::getName()返回线程名称。
Thread::getState()返回线程的状态。例如:NEW(新线程,未开始运行),RUNNABLE(正在运行),BLOCKED(被monitor lock阻塞),WAITING(等待另一个线程),TIMED_WAITING(有时限的等待另一个线程),和TERMINATED(已停止)。
Thread::isAlive()判断该线程是否还在运行。
Thread::isInterrupted()判断线程是否被打断。

我们使用如下的例子简单说明一下这些函数的用法。

public class ThreadInfoExample extends Thread {
    @Override
    public void run() {
        try {
            Thread.sleep(1000); //等待1秒
        } catch (Exception ex) {}
    }

    public static void main(String[] args) {
        Thread newThread = new ThreadInfoExample();
        newThread.start();

        // 打印新线程的唯一标识identifier
        System.out.println("Thread id: " + newThread.getId());

        // 打印新线程的名字
        System.out.println("The new thread is " + newThread.getName());

        // 打印新线程的状态
        System.out.println("The new thread is in " + newThread.getState());

        // 新线程还在运行吗?
        System.out.println("Is the new thread alive? " + newThread.isAlive());

        // 新线程被打断了吗?
        System.out.println("Is the new thread interrupted? " + newThread.isInterrupted());
    }
}

程序运行结果:

> java ThreadInfoExample
Thread id: 12
The new thread is Thread-0
The new thread is in TIMED_WAITING
Is the new thread alive? true
Is the new thread interrupted? false

如果获取线程当前的调用栈信息,请参考Java反射机制章节

3 继承Thread类或者实现Runnable接口?

小水滴认为两种线程创建的方法并无很大的差异,但是,小水滴更偏向于采用实现Runnable接口的方法,理由如下:

  1. 使用Runnable接口能有效分离线程中运行的代码和线程管理功能。在设计上看,如何启动和运行线程与线程中运行的内容是无关的。
  2. Java不支持多重继承。因此,如果新类继承了Thread类的话,则新类无法再继承其他类了。这在某种层度上限制了新类的设计。
  3. 使用Runnable接口更符合Java标准库和第三方库的设计。例如:Executor.execute()接收Runnable对象。

4 总结

本章节通过例子展示了Java语言如何创建和销毁线程。在后续的章节中,我们会逐步进入线程同步和多线程设计的内容。

 

 

上一章
下一章

注册用户登陆后可留言

Copyright  2019 Little Waterdrop, LLC. All Rights Reserved.