多线程并发之入门到门口徘徊(1)上

198 阅读6分钟
相信很多人对于线程的概念都是非常熟悉的。
在Java开发中,我们会很频繁地接触到多线程,比如Spring全家桶框架,底层都是帮我们
扩展了多线程机制,所以对于我们日常开发中,需要去了解多线程,这是通向高级工程师
的必备武器,我们今天就来了解一下多线程。
首先我们还是得了解一下线程的概念。

基础概念

  • 进程

    比如说我们在启动某个应用程序的时候,这个程序在操作系统中就是一个进程。

  • 线程

    对于进程来说,线程就是进程中专门执行进程某个任务的分支,而一个进程通常会有多个任务,它们被进程分派到不同的线程执行,大多数结果会被反馈到进程中,所以数据又被分为了线程私有数据和线程共享数据,共享数据一般存放在进程中,比如JVM虚拟机中,对于final static全局常量(不是所有属性),就是放在运行时常量池中。

    同时对于进程来讲,线程被分为了守护线程和用户线程;用户线程是系统工作线程,比如main线程,用户完成各项逻辑业务;守护线程必须依托于用户线程,比如JVM中的GC机制,当其他所有用户线程关闭后,GC也会被关闭。

在Java Thread类的源码中,有一个属性 private boolean daemon = false,它是决定一个线程是用户线程还是守护线程,false是用户线程,true表示守护线程,可以在线程发出前使用public final void setDaemon(boolean on)来设置线程的性质,但是一旦线程启动,就不能更改,会抛出@throws IllegalThreadStateException if this thread is {@linkplain #isAlive alive}(这是源码上的注释)

Java线程发展过程

Runnable

在JDK1.0的时候,Oracle推出Thread类和Runnable接口,Java初步有了发出多线程的能力,其中Runnable接口有一个方法public abstract void run(),它配合着实现类的start()方法进行线程功能的编写和执行,但是在使用过程中,Runnable并不能将线程执行任务的返回结果集反直接馈到main线程,只能通过共享数据区或者回调函数(匿名实现类或者内部类对特定方法的重写,如重写比较器中的compare方法),但是这样子使得Java代码繁杂,使用共享数据,那就涉及到了数据安全以及CPU资源调配,使用内部类会让代码难懂,加载类的时候就多加载了一个类,于是后面发展出了Future接口。

Future

future有一个实现类:FutrueTask

它同时实现了Runnable、Future接口,还有一个构造器支持Callable,于是它就能执行线程,产生并获取返回值。

到了JDK1.5的时候,Future接口提供了一个get()方法,用于获取线程的结果集,但是这个方法仍然产生了很多现实问题,如在线程发出后,main线程继续执行,当执行到了某个阶段,main线程需要使用之前的线程的结果集,于是它就去从线程中拿(这个拿是我们自己设定的,只能用硬编码),但是线程告诉main还没有执行完,于是main线程就会卡在这一条指令,直到获得结果才能继续执行,于是这里面就产生了一个问题:get()容易阻塞main线程并且抛出异常InterruptedException, ExecutionException。所以Future还有一个方法isDone()来判断询问线程是否完成了任务,这样子就不会有阻塞,但是又产生了另一个问题:轮询会耗费过多的CUP资源,假如线程执行的时间会很长,每次轮询其实都是无用功。

CompletionStage

之前的Future接口中我们可以发现他提供的方法其实是很少的,完全不能对现实各种各样的复杂任务进行操作。

image.png

所以在JDK1.8的版本中,Oracle创造了一个新的接口来对Future进行补齐,并加强了原来的功能,使之更能应对现实复杂的业务

image.png

image.png

CompletableFuture

CompletableFuture是对应CompletionStage的实现类,同时也实现了Future接口,同时JDK1.8也新增了一项Lambda表达式和Stream流的概念,这一更新使得我们的函数式编程更加灵活。

CompletableFuture常用方法解析

创建线程

这个类提供了两类创建线程的方法:supplier

  • runAsync(Runnable runnable)
  • supplyAsync(Supply supplier)
  • runAsync(Runnable runnable,Executor executor)
  • supplyAsync(Supply supplier,Executor executor)

runAsync()方法创建的是没有返回值的线程,所以使用get()/join()是返回的null(这两种方法后面会讲),而executor是自己创建的线程池,没有的话会使用默认的线程池ForkJoinPool

如果你将这段代码拿到idea或者vscode中运行,你就会发现它并没有输出任何值,但是在掘金里运行就会输出“test”,那这是为什么呢?我们先来分析在idea中运行的过程:进入主线程->遇到线程t的创建->t线程在另一个地方执行,main线程继续运行->main线程完成,并被kill;由于主线程被释放了,并且t线程的sout没有输出,那我们可以得出一个结论:这时候的t线程是守护线程,在默认情况下,默认线程池ForkJoinPool 创建出来的线程也是守护线程,而在掘金的运行环境中可能有特殊配置,使得main线程可能也是守护线程,而这是我想说的第二个点:任何线程都是平等的,没有主线程子线程之分,所以所有线程都可以成为守护线程

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
//这可能是掘金的环境
public class test {
    public static void main(String[] args) throws InterruptedException {
        
        test1.main(args);
        TimeUnit.SECONDS.sleep(1);
    }
     
}
class test1{
    public static void main(String[] args) {
        CompletableFuture<String> t = CompletableFuture.supplyAsync(() -> {System.out.println("test");return "test";});
        System.out.println("test1");
    }
}

对此,我们有两种解决方法,第一种,更换线程池(推荐使用),第二种:

Thread thread = new Thread(() -> { t.join(); System.out.println("子线程结束"); });
thread.setDaemon(false);

获取结果集

获取结果集的方法有get(),join(),那这两种方式有什么不同呢?之前说过get()方法是会阻塞线程的,并且会抛出异常,虽然join()也会造成线程阻塞(可以非阻塞获取值需要到后面讲),但是join()有两个好处:

第一个,join()可以在runAsync()创造的线程中使用

第二个,join()不会主动抛出异常

那么从第一点我们可以分析出,join()并不是等待获取结果集,而是等待线程执行完成(虽然它仍然会return结果集),而get()是等待获取结果集

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class test {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        System.out.println(2);
        CompletableFuture t = CompletableFuture.runAsync(() -> {System.out.println("1");});
        t.join();
        System.out.println(2);
    }
}

如果将t.join()屏蔽会输出221,而使用了它就会输出212

这一篇章的内容我先进行到这里,因为本人知识有限,内容可能出现纰漏,所以各位jym请随时提出问题,下一章后续还有很多关于CompletableFuture类的API方法解析。