多线程使用场景
- 通过并行计算提高程序执行性能
- 需要等待网络、I/O 响应导致耗费大量的执行时间,可以采用异步线程的方式来减少阻塞
线程的实现方式
在 Java 中,有多种方式来实现多线程。继承 Thread 类、实现 Runnable 接口、使用 ExecutorService、Callable、Future 实现带返回结果的多线程。如果不需要获取线程的返回结果,推荐使用实现 Runnable 接口的方式。
继承 Thread 类创建线程
看下 Thread 源码,就知道 Thread 是 Runnable 的实现类。
public class ThreadDemo extends Thread {
@Override
public void run() {
System.out.println("继承 Thread 类创建线程");
}
public static void main(String[] args) {
new ThreadDemo().start();
}
}
实现 Runnable 接口创建线程
public class RunnableDemo implements Runnable {
@Override
public void run() {
System.out.println("实现 Runnable 接口创建线程");
}
public static void main(String[] args) {
new Thread(new RunnableDemo()).start();
}
}
实现 Callable 接口通过 FutureTask 包装器来创建 Thread
public class CallableDemo1 implements Callable<String> {
@Override
public String call() throws Exception {
return "实现 Callable 接口通过 FutureTask 包装器来创建 Thread";
}
public static void main(String[] args) {
// 使用单个线程处理Callable
CallableDemo1 callableDemo = new CallableDemo1();
FutureTask<String> futureTask = new FutureTask<>(callableDemo);
new Thread(futureTask).start();
try {
System.out.println(futureTask.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
public class CallableDemo2 implements Callable<String> {
@Override
public String call() throws Exception {
return "实现 Callable 接口通过 FutureTask 包装器来创建 Thread";
}
public static void main(String[] args) {
// 使用线程池处理Callable
CallableDemo2 callableDemo = new CallableDemo2();
ExecutorService executorService = Executors.newFixedThreadPool(10);
Future<String> future = executorService.submit(callableDemo);
try {
System.out.println(future.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
executorService.shutdown();
}
}
public class CallableDemo3 implements Callable<String> {
@Override
public String call() throws Exception {
return "实现 Callable 接口通过 FutureTask 包装器来创建 Thread";
}
public static void main(String[] args) {
// 使用ExecutorCompletionService接收线程池执行的结果
ExecutorService executorService = Executors.newFixedThreadPool(10);
CallableDemo3 callableDemo3 = new CallableDemo3();
CompletionService<String> completionService = new ExecutorCompletionService<>(executorService);
completionService.submit(callableDemo3);
Future<String> future;
try {
future = completionService.take();
System.out.println(future.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
executorService.shutdown();
}
}
线程的状态
Java 线程在运行的生命周期中可能处于6种不同的状态,在给定的一个时刻,线程只能处于其中的一个状态。
状态名称 | 说明 |
---|---|
NEW | 初始状态,线程被构建,但是还没有调用 start() 方法 |
RUNNABLE | 运行状态,Java 线程将操作系统中的就绪和运行两种状态笼统地称作“运行中“ |
BLOCKED | 阻塞状态,表示线程阻塞于锁 |
WAITING | 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断) |
TIMED_WAITING | 超时等待状态,该状态不同于 WAITING,它是可以在指定的时间自行返回的 |
TERMINATED | 终止状态,表示该线程已经执行完毕 |
线程在自身的生命周期中,并不是固定地处于某个状态,而是随着代码的执行在不同的状态之间进行切换,Java 线程状态变迁如图。
下面我们使用 jstack 工具(可以选择打开终端,键入 jstack 或者到 JDK 安装目录的 bin 目录下执行命令),尝试查看示例代码运行时的线程信息,更加深入地理解线程状态,示例代码如下所示。
/**
* 线程状态
*
* @author yinjianwei
* @date 2019/03/13
*/
public class ThreadState {
public static void main(String[] args) {
new Thread(new WaitingThread(), "WaitingThread").start();
new Thread(new TimeWaitingThread(), "TimeWaitingThread").start();
// 使用两个Blocked线程,一个获取锁成功,另一个被阻塞
new Thread(new BlockedThread(), "BlockedThread-1").start();
new Thread(new BlockedThread(), "BlockedThread-2").start();
}
/**
* 等待
*/
static class WaitingThread implements Runnable {
@Override
public void run() {
while (true) {
synchronized (WaitingThread.class) {
try {
WaitingThread.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
/**
* 超时等待
*/
static class TimeWaitingThread implements Runnable {
@Override
public void run() {
while (true) {
try {
TimeUnit.SECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 阻塞
*/
static class BlockedThread implements Runnable {
@Override
public void run() {
synchronized (BlockedThread.class) {
while (true) {
try {
TimeUnit.SECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
运行该示例,打开终端或者命令提示符,键入“jps”,输出如下。
13444 ThreadState
18292 Jps
2308 Test2
15848 Test2
10364 ThreadState
10924
可以看到运行示例对应的进程ID 是13444,接着再键入“jstack 13444”,部分输出如下所示。
// BlockedThread-2 线程阻塞在获取 BlockedThread.class 的锁上
"BlockedThread-2" #13 prio=5 os_prio=0 tid=0x000000001d163000 nid=0x7a4 waiting for monitor entry [0x000000001df5f000]
java.lang.Thread.State: BLOCKED (on object monitor)
// BlockedThread-1 线程获取到了 BlockedThread.class 的锁
"BlockedThread-1" #12 prio=5 os_prio=0 tid=0x000000001d162800 nid=0x3afc waiting on condition [0x000000001de5e000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
// WaitingThread 线程处于等待状态
"WaitingThread" #11 prio=5 os_prio=0 tid=0x000000001d159000 nid=0x3600 in Object.wait() [0x000000001dd5f000]
java.lang.Thread.State: WAITING (on object monitor)
// TimeWaitingThread 线程处于超时等待状态
"TimeWaitingThread" #10 prio=5 os_prio=0 tid=0x000000001d156000 nid=0x3d5c waiting on condition [0x000000001dc5f000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
Daemon 线程
Daemon 线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个 Java 虚拟机中不存在非 Daemon 线程的时候,Java 虚拟机将会退出。可以通过调用 Thread.setDaemon(true) 将线程设置为 Daemon 线程。
注意:Daemon 属性需要在启动线程之前设置,不能在启动线程之后设置。
Daemon 线程被用作完成支持性工作,但是在 Java 虚拟机退出时 Daemon 线程中的 finally 块并不一定会执行,示例代码如下所示。
public class DaemonDemo {
public static void main(String[] args) {
Thread thread = new Thread(new DaemonThread(), "DaemonThread");
thread.setDaemon(true);
thread.start();
}
static class DaemonThread implements Runnable {
@Override
public void run() {
try {
SleepUtils.second(10);
} finally {
System.out.println("DaemonThread finally run.");
}
}
}
}
运行 DaemonDemo 程序,可以看到在终端上没有任何输出,main 线程(非 Daemon 线程)在启动了线程 DaemonThread 之后随着 main 方法执行完毕而终止,而此时 Java 虚拟机中已经没有非 Daemon 线程,虚拟机需要退出。Java 虚拟机中的所有 Daemon 线程都需要立即终止,因此 DaemonThread 立即终止,但是 DaemonThread 中的 finally 块并没有执行。
注意:在构建 Daemon 线程时,不能依靠 finally 块中的内容来确保执行关闭或清理资源的逻辑。
启动和终止线程
启动线程
线程对象在初始化完成之后,调用 start() 方法就可以启动这个线程。
终止线程
线程的 stop() 方法已经被弃用了,该方法终止所有未结束的方法,包括 run 方法。当线程被终止,立即释放被它锁住的所有对象的锁。这会导致对象处于不一致的状态。例如,假定 TransferThread 在从一个账户向另一个账户转账的过程中被终止,钱款已经转出,却没有转人目标账户,现在银行对象就被破坏了。因为锁已经被释放,这种破坏会被其他尚未停止的线程观察到。
利用中断标志(interrupt)
- void interrupt():向线程发送中断请求。线程的中断状态将被设置为 true。如果目前该线程被一个 sleep 调用阻塞,那么,InterruptedException 异常被抛出。
- static boolean interrupted():测试当前线程(即正在执行这一命令的线程)是否被中断。注意,这是一个静态方法。这一调用会产生副作用——它将当前线程的中断状态重置为 false。
- boolean isInterrupted():测试线程是否被终止。不像静态的中断方法,这一调用不改变线程的中断状态。
public class InterruptDemo {
public static void main(String[] args) {
InterruptThread interruptThread = new InterruptThread();
Thread thread = new Thread(interruptThread);
thread.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();
}
private static class InterruptThread implements Runnable {
private int i;
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
i++;
}
System.out.println(i);
}
}
}
利用 boolean 变量
public class ShutdownDemo {
private volatile static boolean stop = false;
public static void main(String[] args) {
ShutdownThread shutdownThread = new ShutdownThread();
Thread thread = new Thread(shutdownThread);
thread.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
stop = true;
}
private static class ShutdownThread implements Runnable {
private int i;
@Override
public void run() {
while (!stop) {
i++;
}
System.out.println(i);
}
}
}
作者:殷建卫 链接:https://www.yuque.com/yinjianwei/vyrvkf/hfsk8i 来源:殷建卫 - 架构笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。