第二章、Java多线程入门类和接口

一、Thread类和Runnable接口

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

    • 继承Thread类,并重写run方法;

    • 实现Runnable接口的run方法;

1.1、继承Thread类

  首先是继承Thread

public class Ex_Thread extends Thread{

    @Override
    public void run() {
        System.out.println("Ex_Thread = " );
    }

    public static void main(String[] args) {
        Ex_Thread ex_thread = new Ex_Thread();
        ex_thread.start();
    }
}

  注意要调用start()方法后,该线程才算启动!

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

1.2、实现Runnable接口

  接着我们来看一下Runnable接口(JDK 1.8 +):

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

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

  示例代码:

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

    public static void main(String[] args) {
        new Thread(new Impl_Runnable()).start();

        new Thread(()->{
            System.out.println("Java 8 匿名内部类");
        }).start();
    }
}

1.3、Thread类构造方法

  • Runnable : 指定要执行的任务;
  • ThreadGroup : 线程组,指定这个线程是在哪个线程组下
  • String: 线程的名字,多个线程的名字是可以重复的。如果不指定名字
//我们大多是直接调用下面两个构造方法:
Thread(Runnable target)
Thread(Runnable target, String name)

1.4、Thread类的几个常用方法

  这里介绍一下Thread类的几个常用的方法:currentThread():静态方法,返回对当前正在执行的线程对象的引用;

    • start()开始执行线程的方法,java虚拟机会调用线程内的run()方法;

    • yield():yield在英语里有放弃的意思,同样,这里的yield()指的是当前线程愿意让出对当前处理器的占用。这里需要注意的是,就算当前线程调用了yield()方法,程序在调度的时候,也还有可能继续运行这个线程的;

      Java线程中的Thread.yield( )方法,译为线程让步。顾名思义,就是说当一个线程使用了这个方法之后,它就会把自己CPU执行的时间让掉,让自己或者其它的线程运行,注意是让自己或者其他线程运行,并不是单纯的让给其他线程。
      yield()的作用是让步。它能让当前线程由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执行权;但是,并不能保
      证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权;也有可能是当前线程又进入到“运行状态”继续运行!
      
      举个例子:一帮朋友在排队上公交车,轮到Yield的时候,他突然说:我不想先上去了,咱们大家来竞赛上公交车。然后所有人就一块冲向公交车,有可能是其他人先上车了,也有可能是Yield先上车了。
      但是线程是有优先级的,优先级越高的人,就一定能第一个上车吗?这是不一定的,优先级高的人仅仅只是第一个上车的概率大了一点而已,
      最终第一个上车的,也有可能是优先级最低的人。并且所谓的优先级执行,是在大量执行次数中才能体现出来的。
    • sleep():静态方法,使当前线程睡眠一段时间;

    • join():使当前线程等待另一个线程执行完毕之后再继续执行,内部调用的是Object类的wait()方法实现的;
      从join方法的源码来看,join方法的本质调用的是Object中的wait方法实现线程的阻塞。调用wait方法必须要获取锁,所以join方法是被synchronized修饰的,synchronized修饰在方法层面相当于synchronized(this),this就是Thread本身的实例。join阻塞的是主线程

    • wait()
      • wait() 是针对已经获取对象锁的线程进行操作。
      • 当线程获取对象锁后,调用 wait() 主动释放对象锁,同时该线程休眠
      • 直到其他线程调用 notify() 唤醒该线程,才继续获取对象锁,并执行
      • 调用 notify() 唤醒线程并不是实时的,而是等相应的 synchronized 语句块执行结束,自动释放对象锁
      • 再由 JVM 选取休眠的线程赋予对象锁,并执行,从而实现线程间同步、唤醒的

1.5、Thread类与Runnable接口的比较:

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

    • 由于Java“单继承,多实现”的特性,Runnable接口使用起来比Thread更灵活。

    • Runnable接口出现更符合面向对象,将线程单独进行对象的封装。

    • Runnable接口出现,降低了线程对象和线程任务的耦合性。

    • 如果使用线程时不需要使用Thread类的诸多方法,显然使用Runnable接口更为轻量。

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

二、Callable、Future与FutureTask

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

  JDK提供了Callable接口与Future接口完成后有一个返回值为我们解决这个问题,这也是所谓的“异步”模型。

2.1、Callable接口

  CallableRunnable类似,同样是只有一个抽象方法的函数式接口。不同的是,Callable提供的方法是有返回值的,而且支持泛型

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

  那一般是怎么使用Callable的呢?Callable一般是配合线程池工具ExecutorService来使用的ExecutorService可以使用submit方法来让一个Callable接口执行。它会返回一个Future,我们后续的程序可以通过这个Futureget方法得到结果。

public class Impl_Callable implements Callable<String> {
    @Override
    public String call() throws Exception {
        // 模拟计算需要一秒
        Thread.sleep(1000);
        return "Impl_Callable";
    }

    public static void main(String[] args) throws Exception{
 
        ExecutorService executor= Executors.newCachedThreadPool();
        Impl_Callable impl_callable = new Impl_Callable();
        // 注意调用get方法会阻塞当前线程,直到得到结果。
        // 所以实际编码中建议使用可以设置超时时间的重载get方法。
        Future<String> result = executor.submit(impl_callable);
        System.out.println(result.get());
    }
}

2.2、Future接口

  Future接口只有几个比较简单的方法:

public abstract interface Future<V> {
    public abstract boolean cancel(boolean paramBoolean);
    public abstract boolean isCancelled();
    public abstract boolean isDone();
    public abstract V get() throws InterruptedException, ExecutionException;
    public abstract V get(long paramLong, TimeUnit paramTimeUnit)
            throws InterruptedException, ExecutionException, TimeoutException;
}

  cancel法是试图取消一个线程的执行。

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

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

2.3、FutureTask类

  上面介绍了Future接口。这个接口有一个实现类叫FutureTaskFutureTask是实现的RunnableFuture接口的,而RunnableFuture接口同时继承了Runnable接口和Future接口:

public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}

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

public class FutrueTask_Demo implements Callable<String> {
    @Override
    public String call() throws Exception {
        Thread.sleep(1000);
        return "FutrueTask_Demo";
    }

    public static void main(String[] args) throws Exception {

        ExecutorService executor = Executors.newCachedThreadPool();
        FutureTask<String> task = new FutureTask<>(new FutrueTask_Demo() );
        executor.submit(task);
        System.out.println(task.get());
    }
}

  使用上与第一个Demo有一点小的区别。首先,调用submit方法是没有返回值的。这里实际上是调用的submit(Runnable task)方法,而上面的Demo,调用的是submit(Callable task)方法。

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

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

2.4、FutureTask的几个状态

/**
  *
  * state可能的状态转变路径如下:
  * NEW -> COMPLETING -> NORMAL
  * NEW -> COMPLETING -> EXCEPTIONAL
  * NEW -> CANCELLED
  * NEW -> INTERRUPTING -> INTERRUPTED
  */
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是任务完成后的瞬时状态。

  以上就是Java多线程几个基本的类和接口的介绍。可以打开JDK看看源码,体会这几个类的设计思路和用途吧!

posted @ 2020-10-30 15:20  jingdy  阅读(167)  评论(0编辑  收藏  举报