令人无限敬仰的 Linux 之父 Linus 说过:
Where the hell do you envision that those magical parallel algorithms would be used(译:你 TMD 究竟是如何想到那些神奇的并行算法会有用呢)
Java 作为一门高级语言,其并发的功能基于 JVM 和操作系统的支持,我们使用并发编程时的学习成本尚且很高,Linus 在做底层并发设计时的复杂和困难可想而知,因此他抱怨和吐槽也就在所难免了。
在这里插入图片描述Java 语言从诞生到现在一直长盛不衰,有很多原因,如自动垃圾回收机制、一次编译,到处运行等等。一门编程语言被发开的根本目的是服务开发者,Java 也一样,因此它屏蔽了很多的底层的细节,让开发者使用起来足够简单、足够便利。在使用 Java 并发编程时,很多知识会涉及到虚拟机甚至操作系统和硬件层面,一个优秀的 Java 程序员都需要了解这些原理,但同时也要注意不能陷入太底层的细枝末节(如具体内核的一些实现、汇编具体实现),那样就是舍本逐末的做法了,我们要做的就是充分了解和理解 Java 并发原理并正确的使用它。本章作为系列的第一章,选择五个常见的 Java 并发编程基础问题进行讲解。
Java 中线程有多种创建方式,其本质是什么
在 Java 中从线程的创建形式上来看有多种,最常见的是“实现 Runnable 接口”,实现了 Runnable 接口的对象实例,需要作为 Thread 类的构造函数的参数传递进去,最终通过 Thread 类的 start() 方法启动线程,也就是说想开线程,一定要经过 Thread 类。
以下在 Java 中使用线程的标准方式:实现 Runnable 接口
//实现 Runnable 接口
Runnable runnable = () -> System.out.println("实现 Runnable 接口");
//作为构造参数传递,赋给 target
Thread thread = new Thread(runnable);
//启动线程
thread.start();
第二种方式是“直接继承 Thread 类”,其实这种方式和第一种本质是相同的,因为 Thread 类本身是实现了 Runnable 接口的,所以继承 Thread 类也相当于间接实现了 Runnable 接口,Thread 类继承关系如下图所示:
在这里插入图片描述继承 Thread 类并重写其 run 方法后,也是直接调用 start() 方法开启线程执行,注意不能调用 run 方法,因为这样只是单纯的执行 run 方法而已,并没有开启一个线程单独执行。
以下在 Java 中继承 Thread 类运行线程的方式:
static class MyThread extends Thread{
@Override
public void run(){
System.out.println("继承自 Thread");
}
}
public static void main(String[] args) {
//"继承自 Thread"
MyThread myThread = new MyThread();
myThread.start();
}
最后一种是针对需要获取线程执行结束后返回值的情况,以上两种方式的 run 方法返回值都是 void,因此要实现获取返回值的情况,就需要“实现 Callable 接口”,并将其包装成一个 FutureTask 传递给 Thread,这里的 FutureTask 继承自 Runnable,也就是说最后开启线程仍然是同样的方式,实现 Callable 启动线程的使用示例如下:
//实现实现 Callable 接口
Callable callable = () -> {
System.out.println("实现 Callable 接口");
return "实现 Callable 接口";
};
//转换为 FutureTask
FutureTask<String> task = new FutureTask<>(callable);
//赋给 target
Thread thread = new Thread(task);
//启动线程
new Thread(task).start();
//阻塞获取执行结果
String result = task.get();
尽管上面有多种方式来开启一个线程,也就是说有多种构造线程的方式,但根据上面的分析,可以看出其实 Java 中线程的“启动方式只有一种,即调用线程 start() 方法”,同样也说也说明:Java 标准类库 java.lang.Thread 是 java 平台对线程的唯一实现,我们看其 Thread 类的 start 源码实现:
//Thread 类的 start() 方法
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
//JNI
private native void start0();
可以看到最终会调用 native 的 start0() 方法中,那线程执行的具体逻辑 run() 方法何时被调用呢?其实通过 JVM 进行资源调度后,会将 Thread 对象映射到操作系统所提供的线程上面去执行,而我们实现 Runnable 或继承 Thread 重写的 run 方法将会在这里被回调,如下图所示:
在这里插入图片描述Thread 类的默认 run 方法代码如下,这里的 target 就是创建 Thread 时构造函数传递的 Runnable 实例:
@Override
public void run() {
if (target != null) {
target.run();
}
}
针对以上这段代码,如果是实现 Runnable 接口,需要将实其以构造函数参数的形式赋给 target,最终调用 target.run(),而 Callable 也会通过 FutureTask 转换后赋给 target 执行,只不过最终要保存执行后的结果,在异步回调一节会专门分析 Callable 和 FutureTask。如果是继承 Thread 类,重写覆盖原有的 run 方法,自然执行的是重写后的 run 方法。
这里需要对前两种方式基本(实现 Runnable 接口和继承 Thread 类)作一个说明,虽然这两种方式本质一样,最终都会通过回调其 run() 执行线程,但从设计模式角度而言,后者实际上基于继承,由于 Java 不支持多继承,如果继承了 Thread 类意味着无法再继承其他类,因此这种方式的使用是受限的。前一种方式则很好的解决这个问题,且这种方式生成的 Runnable 实例可以共享的,也适合在多个线程来处理执行相同逻辑的场景。
综上所述,Java 中线程在本质上只有一种实现的方式,多种方式创建及执行也只是一种封装而已,线程的执行都会调用 star() 方法,进而调用 start0() 方法由虚拟机回调 run() 方法。
在这里插入图片描述守护线程是什么,有什么用处
Java 中的线程分为两类,守护线程和非守护线程,非守护线程也称用户线程,我们默认开的线程一般都是非守护线程。守护线程是一些比较特殊线程,通常处理一些后台支持性的工作,如垃圾回收线程。
我们知道,在正常启动一个普通线程的时候,当这个线程一直处于运行状态,JVM 并不会退出,如下示例是开启一个无线循环的线程,当这个线程启动后,如果不手动停止 JVM,那么就会一直执行下去。
Runnable runnable = () -> {
while (true){
System.out.println("线程在工作");
}
};
Thread thread = new Thread(runnable);
thread.start();
System.out.println("启动线程成功");
运行结果如下:
启动线程成功
线程在工作
线程在工作
线程在工作
线程在工作
线程在工作
线程在工作
线程在工作
…… (无限输出)
上面的示例是开启了一个线程为我们执行业务逻辑,所以 JVM 不退出也理所应当的。但还有一类线程,像比如垃圾回收线程,它是在后台默默为程序回收垃圾,按理说如果这类线程不结束的话,JVM 也无法退出,但明显这类线程需要随着 JVM 的退出而关闭的,因此 JVM 是否退出应不应该依赖于这类线程是否结束,而这类线程就称之为守护线程,其他的线程称为非守护线程。
Java 中规定,如果所有非守护线程退出后,JVM 进程将会退出,因此守护线程不会影响 JVM 的退出,也就是说当 JVM 线程结束时,守护线程会自动关闭,这就免去了继续关闭线程的麻烦。由于守护线程具备这种自动结束生命周期的特性,因此当 JVM 进程退出而一些线程需要自动关闭的时候,则可以将其设置为守护线程。Java 8 中 Completablefuture 的 async 方法默认的用的线程就是守护线程,而普通的线程池用的是用户线程,选择使用时应该注意。守护线程、用户线程、JVM 运行关系如下图:
在这里插入图片描述将一个线程设置为守护线程是通过 thread.setDaemon(true)
方法,这个操作必须在调用开启线程 start() 方法前,否则会抛异常。对上面普通线程的示例增加一行代码 thread.setDaemon(true):
public static void main(String[] args) {
Runnable runnable = () -> {
while (true){
System.out.println("线程在工作");
}
};
Thread thread = new Thread(runnable);
//设置为守护线程
thread.setDaemon(true);
thread.start();
System.out.println("启动线程成功");
}
此时的运行结果如下:
启动线程成功
线程在工作
线程在工作
线程在工作
线程在工作
线程在工作
线程在工作
线程在工作
Process finished with exit code 0
可以看到当把线程设置为守护线程后,运行结果就完全不同了,随着 main 函数的结束,JVM 退出,该线程也随之结束,因此守护线程可以认为守护的是 JVM。那如果在守护线程中,再开启一个线程执行,这个线程是否是守护呢?
public static void main(String[] args) {
Runnable runnable = () -> {
//线程中又开启了一个线程
Runnable runnable1 = () -> {
while (true){
System.out.println("线程在工作");
}
};
Thread thread = new Thread(runnable1);
thread.start();
};
Thread thread = new Thread(runnable);
thread.setDaemon(true);
thread.start();
System.out.println("启动线程成功");
}
}
执行结果如下
启动线程成功
线程在工作
Process finished with exit code 0
可以看到主函数退出后线程也随即退出了,因此在守护线程中开启的线程默认都是守护线程。
image如何理解线程的中断
Java 中常见的中断方式是“调用一个线程对象的 interrupt 方法”,线程的中断是线程间的一种协作机制,从字面上理解,中断似乎就是让目标线程立刻停止执行的意思,实际上并非如此。
一个线程向另一个线程发起中断,目标线程收到中断信号后会设置中断标识位,它并不会直接退出,也就是说如何处理完全由目标线程自行决定的,目标线程可以选择在在合适的时机中断自己,这好比一个人收到另一个人打来的招呼,可以选择不予理会,也可以选择稍后在回应。
比如在下面的代码示例中,一个线程内部在 while(true) 死循环,此时另一个线程(对应代码中主线程)对其发起中断,死循环的线程并不会结束退出。
public static void main(String[] args) {
Runnable runnable = () -> {
while (true){
System.out.println("线程在执行");
}
};
Thread thread = new Thread(runnable);
thread.start();
//中断线程
thread.interrupt();
System.out.println("线程中断成功");
}
执行结果如下:
线程中断成功
线程在工作
线程在工作
线程在工作
线程在工作
线程在工作
线程在工作
…… (无限输出)
从上面的示例可以看出,中断对该线程似乎毫无作用,那中断的意义是什么?其实还是前文提到的,“Java 中断提供的是一种柔和的协作机制”,这样做的好处是增加了代码健壮性,如果采用生硬的强制线程退出的话,会在开发中带来很多如资源未及时释放等严重的问题。
这种协作机制要如何来完成呢,在我们对一个线程发起中断后,虽然目标线程可以选择什么都不做,但虚拟机做了一件重要的事情,就是在 JVM 内部设置一个中断标识。这个标识本身并不是 Thread 类中的成员变量,因此我们无法看到它,但 Thread 类提供了一个 native 的方法 boolean isInterrupted() 来获取线该标识位,如下所示,默认是查看线程中断状态不清除中断标。
//Thread 类
public boolean isInterrupted() {
return isInterrupted(false);
}
private native boolean isInterrupted(boolean ClearInterrupted);
因此中断的协作机制如下图这样:
在这里插入图片描述在 Java 中,涉及到线程阻塞的方法中一般声明抛出 InterruptedException 来响应中断,也就是当线程调用以下方法进入阻塞后,如果当有其他线程发来中断请求,阻塞的线程会抛出 InterruptedException 来响应中断,当然这些都是 JVM 为我们实现的,这类方法声明如下:
public static native void sleep(long millis) throws InterruptedException;
public final native void wait(long timeout) throws InterruptedException;
当然我们在自己定义的线程方法中也要对中断进行处理时,可以在某个合适的时机(如该任务是循环处理,在每次循环的时候判断是否被中断了)调用 isInterrupt() 方法检测自身是否被中断进而进而响应,这完全由用户自己来决定。因此将之前的 while 循环做如下处理使其支持中断。下面是一个中断的简单示例:
public static void main(String[] args) {
Runnable runnable = () -> {
while (true){
//判断线程是否被中断
if(Thread.currentThread().isInterrupted()){
System.out.println("线程中断退出");
break;
}
System.out.println("线程在执行");
}
};
Thread thread = new Thread(runnable);
thread.start();
//中断线程
thread.interrupt();
System.out.println("线程中断成功");
}
如上面的例子所示,增加了判断线程是否被中断的操作,此时当主线程发起中断后,该线程也就退出循环了。
网友评论