美文网首页
Java 并发面试系列:并发编程基础

Java 并发面试系列:并发编程基础

作者: you的日常 | 来源:发表于2020-12-04 14:46 被阅读0次

    令人无限敬仰的 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("线程中断成功");
    }
    
    

    ​如上面的例子所示,增加了判断线程是否被中断的操作,此时当主线程发起中断后,该线程也就退出循环了。

    相关文章

      网友评论

          本文标题:Java 并发面试系列:并发编程基础

          本文链接:https://www.haomeiwen.com/subject/ooyubktx.html