凌天阁--多线程系列之入门使用(二)

本周小宋买了东方新能源的基金,运气还是不错的,连涨五天小赚一笔,可以愉快的过一个周末了,连写博客的热情都更加高涨了呢🤣。进入正题我们今天讲一下多线程的入门使用。


在上一篇博客中大家了解了操作系统中进程和多线程的基本概念。那么在Java中,我们是如何使用多线程的呢?

Thread类和Runnable接口

首先,我们需要有一个线程类。JDK提供了Thread类和Runnable接口来让我们实现自己的线程类。

  1. 继承Thread类,并重写run方法。
  2. 实现Runnable接口的run方法。

接着小宋会带大家学习如何使用Thread类和Runnable接口来写一个java多线程程序。

继承Thread类

首先我们先看看如何继承Thread类

public class Demo {
    public static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("MyThread");
        }
    }

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

调用start方法后,该线程启动。

我们在程序里面调用了start()方法后,虚拟机会先为我们创建一个线程处于就绪状态,然后等到这个线程第一次得到cpu分配的时间片时再调用run()方法启动。
注意不可多次调用start()方法。在第一次调用start()方法后,再次调用start()方法会抛出异常。

实现Runnable接口

接着我们来看一下jdk1.8以上的Runnable接口

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

大家可以看到Runnable是一个函数式接口,这就意味着我们可以使用Java 8的函数式编程来简化代码。
eg.

public class Demo {
    public static class MyThread implements Runnable {
        @Override
        public void run() {
            System.out.println("MyThread");
        }
    }

    public static void main(String[] args) {
		
		//传统方式
        new Thread(new MyThread()).start();

        // Java 8 函数式编程(lambda表达式),可以省略MyThread类,因为Thread类本身就实现了Runnable接口
        new Thread(() -> {
            System.out.println("Java 8 匿名内部类");
        }).start();
    }
}

Thread类构造方法

Thread类是一个Runnable接口的实现类。
在这里插入图片描述
接着我们来看看Thread类的源码:

查看Thread类的构造方法,发现其实是简单调用一个私有的init方法来实现初始化。
init方法:
在这里插入图片描述
Thread构造函数调用init方法:
在这里插入图片描述
使用在init方法里初始化AccessControlContext类型的私有属性
在这里插入图片描述
在这里插入图片描述
两个用于支持ThreadLocal的私有属性
在这里插入图片描述
接着我们来解释一下init方法的参数:

  • g:线程组,指定这个线程是在哪个线程组下;

  • target:指定要执行的任务;

  • name:线程的名字,多个线程的名字是可以重复的。如果不指定名字,见Thread构造函数调用init方法源码

  • acc:在init方法用于初始化私有变量inheritedAccessControlContext。上面有源码

    inheritedAccessControlContext变量有点神奇。它是一个私有变量,但是在Thread类里只有init方法对它进行初始化,在exit方法把它设为null。
    其它没有任何地方使用它。一般我们是不会使用它的,那什么时候会使用到这个变量呢?
    可以参考这个stackoverflow的问题:Restrict permissions to threads which execute third party software.

  • inheritThreadLocals:可继承的ThreadLocal,见上面最后的源码,Thread类里面有两个私有属性来支持ThreadLocal。

实际上,我们大多使用的时候都是调用下面这两个构造函数:
在这里插入图片描述
在这里插入图片描述

Thread类的几个常用方法

这里介绍一下Thread类的几个常用的方法:

  • currentThread():静态方法,返回对当前正在执行的线程对象的引用;
  • start():开始执行线程的方法,java虚拟机会调用线程内的run()方法;
  • yield():yield有屈服放弃的意思,同样,这里的yield()指的是当前线程愿意让出对当前处理器的占用。这里需要注意的是,就算当前线程调用了yield()方法,程序在调度的时候,也还有可能继续运行这个线程的;
  • sleep():静态方法,使当前线程睡眠一段时间;
  • join():使当前线程等待另一个线程执行完毕之后再继续执行,内部调用的是Object类的wait方法实现的;

Thread类与Runnable接口的比较

我们在实现一个自定义的线程类,可以有继承Thread类或者实现Runnable接口这两种方式,它们之间有什么优劣呢?

  • 由于Java“单继承,多实现”的特性,Runnable接口使用起来比Thread更灵活。
  • Runnable接口更符合面向对象,将线程单独进行对象的封装。
  • Runnable接口出现,降低了线程对象和线程任务的耦合性。
  • 如果使用线程时不需要使用Thread类的诸多方法,使用Runnable接口更为轻量。

所以,我们通常优先使用实现Runnable接口这种方式来自定义线程类

Callable、Future与FutureTask

通常来说,我们使用Runnable和Thread来创建一个新的线程。但是它们有一个弊端,就是run方法是没有返回值的。而有时候我们希望开启一个线程去执行一个任务,并且这个任务执行完成后有一个返回值。

JDK提供了Callable接口与Future接口为我们解决这个问题,这也是所谓的“异步”模型。

Callable接口

Callable与Runnable类似,同样是只有一个抽象方法的函数式接口。不同的是,Callable提供的方法是有返回值的,而且支持泛型。
在这里插入图片描述
那一般是怎么使用Callable的呢?Callable一般是配合线程池工具ExecutorService来使用的。会在后面的博客中讲解线程池的使用。这里只介绍ExecutorService可以使用submit方法来让一个Callable接口执行。它会返回一个Future,我们后续的程序可以通过这个Future的get方法得到结果。

eg.

// 自定义Callable
class Task implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        // 模拟计算需要一秒
        Thread.sleep(1000);
        return 2;
    }
    public static void main(String args[]) throws Exception {
        // 使用
        ExecutorService executor = Executors.newCachedThreadPool();
        Task task = new Task();
        Future<Integer> result = executor.submit(task);
        // 注意调用get方法会阻塞当前线程,直到得到结果。
        // 所以实际编码中建议使用可以设置超时时间的重载get方法。
        System.out.println(result.get()); 
    }
}

最后输出: 2

Future接口

Future接口只有几个比较简单的方法
在这里插入图片描述
在这里插入图片描述
cancel方法是试图取消一个线程的执行。

注意是试图取消,并不一定能取消成功。因为任务可能已完成、已取消、或者一些其它因素不能取消,存在取消失败的可能。boolean类型的返回值是“是否取消成功”的意思。参数mayInterruptIfRunning表示是否采用中断的方式取消线程执行。

所以有时候,为了让任务有能够取消的功能,就使用Callable来代替Runnable。如果为了可取消性而使用 Future但又不提供可用的结果,则可以声明 Future<?>形式类型、并返回 null作为底层任务的结果。

FutureTask类

在上面已经讲解了Future接口。这个接口有一个实现类叫FutureTask。FutureTask是实现的RunnableFuture接口的,而RunnableFuture接口同时继承了Runnable接口和Future接口:
在这里插入图片描述
在这里插入图片描述
那FutureTask类有什么用?为什么要有一个FutureTask类?

前面说到了Future只是一个接口,而它里面的cancel,get,isDone等方法要自己实现起来都是非常复杂的。所以JDK提供了一个FutureTask类来供我们使用。

eg.

// 自定义Callable,和上面Callable案例一样
class Task implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        // 模拟计算需要一秒
        Thread.sleep(1000);
        return 2;
    }
    public static void main(String args[]) throws Exception {
        // 使用
        ExecutorService executor = Executors.newCachedThreadPool();
        FutureTask<Integer> futureTask = new FutureTask<>(new Task());
        executor.submit(futureTask);
        System.out.println(futureTask.get());
    }
}

使用上与上面Callable的案例有一点小的区别。首先,调用submit方法是没有返回值的,因为这里的FutureTask实现的是RunnableFuture接口,然后RunnableFuture接口又继承了Runnable接口没有返回值。这里实际上是调用的是ExecutorService的submit(Runnable task)方法,而上面的Callable的案例,调用的是submit(Callable task)方法。
在这里插入图片描述

然后,这里是使用FutureTask直接get取值,而上面的Callable案例是通过submit方法返回的Future去取值。

在很多高并发的环境下,有可能Callable和FutureTask会创建多次。FutureTask能够在高并发环境下确保任务只执行一次。可以多去看FutureTask源码。

FutureTask的几个状态

在这里插入图片描述

private volatile int state;
private static final int NEW          = 0;
private static final int COMPLETING   = 1;
private static final int NORMAL       = 2;
private static final int EXCEPTIONAL  = 3;
private static final int CANCELLED    = 4;
private static final int INTERRUPTING = 5;
private static final int INTERRUPTED  = 6;

state表示任务的运行状态,初始状态为NEW。运行状态只会在set、setException、cancel方法中终止。COMPLETING、INTERRUPTING是任务完成后的瞬时状态。

讲到这里本章对多线程入门的一些类和接口使用的讲解也就结束了,如果想了解更多知识可以在对应的专栏中看系列文章,谢谢大家的观看,希望能给各位同学带来帮助。如果觉得博主写的还可以的,可以点赞收藏。 😉

posted @ 2020-12-18 18:56  奋斗的小宋  阅读(13)  评论(0)    收藏  举报