Java中的多线程

一、线程的创建方式

JUC中为Java多线程提供了很多工具,其中创建线程有四种方式:

  • 继承Thread类并重写run()方法:
    开发者可以自定义一个类继承Thread类,并重写run()方法。然后创建该类的实例,并调用start()方法执行。
    class MyThread extends Thread {
    @Override
    public void run() {
    System.out.println(“Thread running”);
    }
    }
    MyThread t = new MyThread();
    t.start(); // 启动线程
  • 实现Runnable接口并传给Thread的构造函数:
    一个类不能继承多个类,但是可以实现多个接口。因此可以将目标类实现Runnable接口,并将其传给Thread构造器(也可以直接传lambda表达式给构造器)。
    Runnable task = () -> System.out.println(“Runnable running”);
    Thread t = new Thread(task);
    t.start(); // 启动线程
  • 实现Callable接口,并采用FutureTask类包装,最后传入Thread构造器:
    实现Callable接口可以通过FutureTask实例获取线程执行的返回值。
    Callable task = () -> {
    System.out.println(“Callable running”);
    return 42;
    };
    FutureTask futureTask = new FutureTask<>(task);
    Thread t = new Thread(futureTask);
    t.start(); // 启动线程
  • 通过Executor框架创建线程池:
    通过submit()或者execute()方法执行任务,避免反复创建和销毁线程。
    ExecutorService executor = Executors.newFixedThreadPool(2);
    executor.submit(() -> System.out.println(“Executor running”));
    executor.shutdown();

二、线程的生命周期和状态

Java的线程生命周期中有以下六种状态:

  • 新建(NEW)
    当一个线程对象被创建时,Java不会立刻启动这个线程,而是将这个线程置于新建状态下。
  • 可运行(RUNNABLE)
    当这个线程对象执行start()方法时,就会进入可运行状态。
    tip :可运行状态不代表线程正在运行,线程正在等待时间片分配也是可运行状态。
  • 阻塞(BLOCKED)
    当线程试图进入一个同步代码块,但是此时所获取的锁被其它线程占用时,就会进入阻塞状态。
  • 等待(WAITING)
    当线程调用Object.wait()Thread.join()方法时,线程就会进入等待状态,直到被notify()notifyAll()方法唤醒。
  • 超时等待(TIMED_WAITING)
    如果线程调用了带超时时间的方法,如 Thread.sleep(long millis)wait(long timeout)join(long millis),就会进入超时等待状态。
  • 终止(TERMINATED)
    run()方法执行完毕或未捕获异常而提前结束时,线程就会进入终止状态。

流程如图:
线程的生命周期

三、线程上下文的切换

线程上下文的切换 ,指的是CPU从一个线程切换到另一个线程是,操作系统需要保存当前线程的执行状态,并加载下一个线程的执行状态,以便它们能够正确运行。执行状态有:寄存器状态程序计数器状态栈信息线程优先级 等。

发生时机

有四种情况:

  • CPU时间片耗尽
    操作系统为每个线程都分配了一定的时间片,当规定时间片内线程没有执行完毕自身任务时,操作系统会强制切换到下一线程,触发线程上下文切换。
  • 线程主动让出CPU
    当线程执行到Three.sleep()Object.wait()等方法时,线程会主动让出CPU,触发线程上下文切换。
  • 调用了阻塞类型的系统中断
    当线程进行IO操作时,因为IO操作通常需要等待外部资源,所以线程会被挂起,触发线程上下文切换。
  • 被结束或者终止运行

线程上下文切换虽然可以实现多任务并发执行,但是它也会带来CPU时间消耗缓存失效资源竞争 等问题,增大了性能损耗。

切换过程

  1. 保存当前线程上下文至内存 :包括寄存器状态、程序计数器状态、栈信息等。
  2. 根据线程调度算法 ,找到下一个要执行的线程:调度算法包括时间片轮转优先级调度 等。
  3. 加载下一个线程的上下文信息 :从内存中恢复寄存器状态、程序计数器状态和栈信息。
  4. CPU执行被加载线程的代码