Skip to content

基础篇 - Java 线程类 ​

Thread 类和 Runnable 接口 ​

在 Java 中,我们是如何使用多线程的呢?

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

  • 继承 Thread 类,并重写 run 方法;
  • 实现 Runnable 接口的 run 方法。

继承 Thread 类 不推荐 ​

继承 java.lang.Thread 类(Thread 类实现了 Runnable 接口),并重写 run 方法。

java
@Slf4j
public class ConcreteThread extends Thread {

    public ConcreteThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        log.info("{} - {}", Thread.currentThread().getName(), Thread.currentThread().getId());
    }

}

@Test
void threadTest() {
    Thread thread = new ConcreteThread("ConcreteThread");
    thread.start();
}
@Slf4j
public class ConcreteThread extends Thread {

    public ConcreteThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        log.info("{} - {}", Thread.currentThread().getName(), Thread.currentThread().getId());
    }

}

@Test
void threadTest() {
    Thread thread = new ConcreteThread("ConcreteThread");
    thread.start();
}

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

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

注意不可多次调用 start() 方法。在第一次调用 start() 方法后,再次调用 start() 方法会抛出 IllegalThreadStateException 异常。

实现 Runnable 接口推荐 ​

实现 java.lang.Runnable 接口,并重写 run() 方法。通过 Thread 调用 start() 方法来启动线程。

Runnable 接口定义:

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

示例代码:

java
@Slf4j
public class ConcreteRunnable implements Runnable {

    @Override
    public void run() {
        log.info("{} - {}", Thread.currentThread().getName(), Thread.currentThread().getId());
    }
}

@Test
void runnableTest() {
    Thread thread = new Thread(new ConcreteRunnable(), "ConcreteRunnableThread");
    thread.start();

     // Java 8 函数式编程,可以使用 Java 8 的函数式编程
    new Thread(() -> {
        System.out.println("Java 8 匿名内部类");
    }).start();
}
@Slf4j
public class ConcreteRunnable implements Runnable {

    @Override
    public void run() {
        log.info("{} - {}", Thread.currentThread().getName(), Thread.currentThread().getId());
    }
}

@Test
void runnableTest() {
    Thread thread = new Thread(new ConcreteRunnable(), "ConcreteRunnableThread");
    thread.start();

     // Java 8 函数式编程,可以使用 Java 8 的函数式编程
    new Thread(() -> {
        System.out.println("Java 8 匿名内部类");
    }).start();
}

推荐使用这种方法,因为单继承、多实现,实现接口并不会影响类继承其他的类,而继承 Thread 接口会无法继承其他的业务类。

Callable、Future 与 FutureTask ​

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

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

实现 Callable 接口 适用有返回值 ​

与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。

java
@Slf4j
public class ConcreteCallable implements Callable<String> {

    @Override
    public String call() throws Exception {
        log.info("{} - {}", Thread.currentThread().getName(), Thread.currentThread().getId());
        return "callback value";
    }
}

@Test
@SneakyThrows
void CallableTest() {
    ConcreteCallable callable = new ConcreteCallable();
    FutureTask task = new FutureTask(callable);
    Thread callableThread = new Thread(task, "CallableThread");
    callableThread.start();
    log.info("Call value: {}", task.get());
}
@Slf4j
public class ConcreteCallable implements Callable<String> {

    @Override
    public String call() throws Exception {
        log.info("{} - {}", Thread.currentThread().getName(), Thread.currentThread().getId());
        return "callback value";
    }
}

@Test
@SneakyThrows
void CallableTest() {
    ConcreteCallable callable = new ConcreteCallable();
    FutureTask task = new FutureTask(callable);
    Thread callableThread = new Thread(task, "CallableThread");
    callableThread.start();
    log.info("Call value: {}", task.get());
}

Future 接口 ​

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

java
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;
}
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 作为底层任务的结果。

FutureTask 类 ​

FutureTask是实现的RunnableFuture接口的,而RunnableFuture接口同时继承了Runnable接口和Future接口:

java
public interface RunnableFuture<V> extends Runnable, Future<V> {
    /**
     * Sets this Future to the result of its computation
     * unless it has been cancelled.
     */
    void run();
}

public class FutureTask<V> implements RunnableFuture<V> {

    public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }

    // 执行 Runnable 的话,需要指定固定的返回值
    public FutureTask(Runnable runnable, V result) {
        this.callable = Executors.callable(runnable, result);
        this.state = NEW;       // ensure visibility of callable
    }
}
public interface RunnableFuture<V> extends Runnable, Future<V> {
    /**
     * Sets this Future to the result of its computation
     * unless it has been cancelled.
     */
    void run();
}

public class FutureTask<V> implements RunnableFuture<V> {

    public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }

    // 执行 Runnable 的话,需要指定固定的返回值
    public FutureTask(Runnable runnable, V result) {
        this.callable = Executors.callable(runnable, result);
        this.state = NEW;       // ensure visibility of callable
    }
}

CompletableFuture 类 ​

在 Java 8 中,新增加了一个包含 50 个方法左右的类:CompletableFuture,结合了 Future 的优点,提供了非常强大的 Future 的扩展功能,可以帮助我们简化异步编程的复杂性,提供了函数式编程的能力,可以通过回调的方式处理计算结果,并且提供了转换和组合 CompletableFuture 的方法。

CompletableFuture 被设计在 Java 中进行异步编程。异步编程意味着在主线程之外创建一个独立的线程,与主线程分隔开,并在上面运行一个非阻塞的任务,然后通知主线程进展,成功或者失败。

通过这种方式,你的主线程不用为了任务的完成而阻塞/等待,你可以用主线程去并行执行其他的任务。使用这种并行方式,极大地提升了程序的表现。

java
public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {

}
public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {

}

四种静态实例化方法:

java
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier);
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor);

public static CompletableFuture<Void> runAsync(Runnable runnable);
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor);
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier);
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor);

public static CompletableFuture<Void> runAsync(Runnable runnable);
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor);

有两种格式,一种是 supply 开头的方法,一种是 run 开头的方法

  • supply 开头:这种方法,可以返回异步线程执行之后的结果
  • run 开头:这种不会返回结果,就只是执行线程任务

如果不指定线程池,默认使用 ForkJoinPool 的公共线程池 ForkJoinPool.commonPool()。

使用 CompletableFuture 场景

  • 执行比较耗时的操作时,尤其是那些依赖一个或多个远程服务的操作,使用异步任务可以改善程序的性能,加快程序的响应速度。
  • 使用 CompletableFuture 类,它提供了异常管理的机制,让你有机会抛出、管理异步任务执行种发生的异常。
  • 如果这些异步任务之间相互独立,或者他们之间的的某一些的结果是另一些的输入,你可以讲这些异步任务构造或合并成一个。

Thread 常用方法 ​

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

start() ​

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

WARNING

注意不可多次调用 start() 方法。在第一次调用 start() 方法后,再次调用 start() 方法会抛出 IllegalThreadStateException 异常。

yield() ​

暂停当前正在执行的线程对象(及放弃当前拥有的 cup 资源),并执行其他线程。yield() 做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用 yield() 的目的是让相同优先级的线程之间能适当的轮转执行。

但是,实际中无法保证 yield() 达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。

sleep() ​

使当前正在执行的线程暂停指定的毫秒数,也就是进入休眠的状态。需要注意的是,sleep 的时候要对异常进行处理。

java
try {
    Thread.sleep(1000L); //暂停 1 秒
} catch (InterruptedException e) {
    e.printStackTrace();
}
try {
    Thread.sleep(1000L); //暂停 1 秒
} catch (InterruptedException e) {
    e.printStackTrace();
}

join() ​

等待这个线程执行完才会轮到后续线程得到 cpu 的执行权,使用这个也要抛出异常。

java
@Test
void joinTest() throws InterruptedException {
    Thread t1 = new Thread(() -> {
        try {
            for (int i = 0; i < 5; i++) {
                log.info("Thread 1 ding ding ding " + i);
                Thread.sleep(1000L);
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            log.info("Thread 1 exit");
        }
    });
    Thread t2 = new Thread(() -> {
        try {
            // 关键语句:需要等 t1 执行关闭,才会开始执行 t2 剩下的逻辑
            t1.join();
            for (int i = 0; i < 5; i++) {
                log.info("Thread 2 ding ding ding " + i);
                Thread.sleep(1000L);
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            log.info("Thread 2 exit");
        }
    });
    t1.start();
    t2.start();
    // 让 t2 执行完,主线程再退出
    t2.join();
}
@Test
void joinTest() throws InterruptedException {
    Thread t1 = new Thread(() -> {
        try {
            for (int i = 0; i < 5; i++) {
                log.info("Thread 1 ding ding ding " + i);
                Thread.sleep(1000L);
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            log.info("Thread 1 exit");
        }
    });
    Thread t2 = new Thread(() -> {
        try {
            // 关键语句:需要等 t1 执行关闭,才会开始执行 t2 剩下的逻辑
            t1.join();
            for (int i = 0; i < 5; i++) {
                log.info("Thread 2 ding ding ding " + i);
                Thread.sleep(1000L);
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            log.info("Thread 2 exit");
        }
    });
    t1.start();
    t2.start();
    // 让 t2 执行完,主线程再退出
    t2.join();
}

这里有两处使用了 join() 方法:

  • 第一处在 t2 的 run 方法中,使用 t1.join(),来让 t1 和 t2 串起来,按照顺序执行,所以 t2 会在 t1 执行完毕后再执行;
  • 第二处,在主线程中,使用 t2.join(),可以让主线程在 t2 执行完毕后再退出,而不是直接退出。

扩展:让主线程等待子线程执行完毕再退出的方法

  1. 使用 sleep() 方法让主线程等待,但是不好控制需要休眠的时间;
  2. 使用 join() 方法,让主线程等待子线程执行完毕再继续往下执行;
  3. 使用 java.util.concurrent.CountDownLatch 统计线程数,每个线程执行完后进行 countDown() 操作,主线程使用 await() 等待计数变为 0 再往下执行;
  4. 使用 java.util.concurrent.CyclicBarrier 同步多个线程的操作;
  5. 使用线程池的 isTerminated() 方法,判断所有子线程是否都执行完,注意需要先执行 shutdown(),否则永不为 true。

setDaemon() ​

将此线程标记为守护线程,准确来说,就是服务其他的线程,像 Java 中的垃圾回收线程,就是典型的守护线程。

java
@Test
void daemonTest() throws InterruptedException {
    Thread t1 = new Thread(() -> {
        try {
            for (int i = 0; i < 5; i++) {
                log.info("Thread 1 ding ding ding " + i);
                Thread.sleep(1000L);
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            log.info("Thread 1 exit");
        }
    });
    Thread t2 = new Thread(() -> {
        try {
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            log.info("Thread 2 exit");
        }
    });
    t1.setDaemon(true);
    t1.start();
    t2.start();
    Thread.sleep(2000L);
}
@Test
void daemonTest() throws InterruptedException {
    Thread t1 = new Thread(() -> {
        try {
            for (int i = 0; i < 5; i++) {
                log.info("Thread 1 ding ding ding " + i);
                Thread.sleep(1000L);
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            log.info("Thread 1 exit");
        }
    });
    Thread t2 = new Thread(() -> {
        try {
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            log.info("Thread 2 exit");
        }
    });
    t1.setDaemon(true);
    t1.start();
    t2.start();
    Thread.sleep(2000L);
}

如果其他线程都执行完毕,main 方法(主线程)也执行完毕,JVM 就会退出,也就是停止运行。如果 JVM 都停止运行了,守护线程自然也就停止了。

参考资料 ​