Java 多线程并发【2】线程

并发与线程

并发并不一定依赖多线程,比如进程也可以并发。但 Java 中的并发多数都与线程有关。

线程是操作系统中的概念,是比进程更轻量级的调度执行基本单位。主流的操作系统都提供了线程的实现,而 Java 作为一门跨平台语言,自然是提供了对线程的统一处理。

在 Java 中对线程的抽象是 Thread 类:

public class Thread implements Runnable

程序中的执行线程,JVM 允许应用程序同时运行多个执行线程。每个线程都有一个优先级。高优先级的线程先于低优先级线程执行。

而在 Java 中,执行了 start() 方法且还没执行结束的 Thread 实例代表一个线程。

Thread 类的很多操作方法都带有 native 关键字。这个关键字代表这个方法的实现与 Native 层相关。实现线程的主要方式有三种:

  1. 使用内核线程实现
  2. 使用用户线程实现
  3. 使用用户线程 + 轻量级进程混合实现

使用内核线程实现线程

内核线程 (Kernel-Level Thread,KLT)是由操作系统的内核支持的线程,这种线程由内核来完成线程切换。内核具有一个线程调度器,通过调度器将内核线程映射到处理器上。

每个内核线程可以视为一个内核的分身,这样操作系统就具备了同时处理多个任务的能力,支持多线程的内核称之为多线程内核。

内核线程与线程的关系

程序一般不会直接去使用内核线程,而是使用内核线程的高级 API — 轻量级进程(LightWeight Process,LWP)。轻量级进程就是我们通常意义上说的线程,每个轻量级进程都由一个内核线程支持,因此必须先支持内核线程,才能有轻量级进程。

下图是进程(P)、轻量级进程(LWP)、内核线程(KLT)、线程调度器和 CPU 的关系。

Java 多线程并发【2】线程一个进程有多个轻量级进程,每个轻量级进程都是一个独立的调度单元,所以不存在一个轻量级进程阻塞,导致整个进程阻塞的情况出现。

轻量级进程(线程)的缺点

但是轻量级进程有它的局限性:轻量级进程是由内核线程实现的,所以线程操作都需要进行系统调用,而系统调用代价相对较高,需要在用户态和内核态来回切换,会消耗一定的内核资源,因此一个系统支持的轻量级进程的数量是有限的。

使用用户线程实现线程

广义上讲,一个线程只要不是内核线程,就可以认为是用户线程(User Thread, UT)。轻量级进程虽然对应一个内核线程,但它本身并不是内核线程,所以它属于用户线程。虽然如此,但它的操作与内核线程相关,还是会消耗系统资源。

这里用户线程的概念指的是狭义上的概念。在狭义上,用户线程指完全建立在用户空间的线程库上,内核不关系线程的存在与实现。用户线程的创建、同步、销毁等都是在用户态中完成的,不需要内核线程支持。如果程序实现得当,这种线程不需要切换到内核态,这种操作是非常迅速且高效的,也就可以支持更大数量的线程同时调用。

Java 多线程并发【2】线程
img

没有内核线程的支持对于用户线程来说是双刃剑。没有内核态的切换,更加高效且能够支持更大的并发数量;而劣势则是所有的线程操作都是需要程序自己来处理。

而且由于操作系统只把处理器资源分配到进程,那诸如“阻塞如何处理”、“多处理器系统中如何将线程映射到其他处理器上”这类问题解决起来将会异常困难,甚至不可能完成。

使用用户线程 + 轻量级进程混合实现

线程除了依赖内核线程实现和完全由用户程序自己实现之外,还有一种将内核线程与用户线程一起使用的实现方式。

在这种混合实现下,既存在用户线程,也存在轻量级进程。

  • 用户线程还是完全建立在用户空间中,因此用户线程的创建、切换、析构等操作依然廉价,并且可以支持大规模的用户线程并发。
  • 操作系统提供支持的轻量级进程则作为用户线程和内核线程之间的桥梁,这样可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级线程来完成,大大降低了整个进程被完全阻塞的风险。

在这种混合模式中,用户线程与轻量级进程的数量比是不定的,即为N∶M的线程模型:

Java 多线程并发【2】线程
img

这种实现思想的一个例子就是 Kotlin 中的协程库。协程库实际上就是实现了一套用户线程管理和调度逻辑,然后依附于 Java 中的线程执行。

Java 线程的实现

Java 中的线程实际上就是轻量级进程,它的一些操作方法都带有 native 关键字,需要依附于操作系统的原生实现。

但 JVM 规范并没有限定 Java 线程用哪种线程模型(这里指 用户线程与轻量级线程、内核进程的数量对应关系)来实现,所以不同的 JVM 可能会有所不同。

对于甲骨文的 JDK (Sun JDK)来说,它的 Windows 版与 Linux 版都是使用一对一的线程模型实现的,一条 Java 线程就映射到一条轻量级进程之中,因为 Windows 和 Linux 系统提供的线程模型就是一对一的。

Java 线程调度

线程调度指系统为线程分配处理器使用权的过程,主要分为两种:协同式线程调度和抢占式线程调度。

协同式线程调度

线程执行完成后,主动通知系统切换到其他线程。

缺点:一个线程完成所有事件后才释放资源,时间不可控,如果正在执行的线程出现问题,一直不释放 CPU 资源,会导致阻塞。

抢占式线程调度

每个线程的执行时间由系统进行分配。线程的切换不由线程自身决定。在这种方案下,线程的执行时间是可控的。Java 采取的就是这种调度方式。

虽然在这种模式下,执行时间完全交由系统,但是我们也有一些影响调度的办法:

  • 为线程设置优先级,优先级高系统会多分配一些时间。
  • Thread.yield() 主动让出 CPU 资源。

线程优先级并不能保证一定有作用,因此 Java 的线程是映射到系统的原生线程来实现,所以最终调度还是由操作系统决定。不同平台实现可能不一致。

Java 线程的状态与转换

Java 为线程定义了六种状态:

public enum State {
    // 【新建】,线程还没有调用start方法前的状态
    NEW,

    // 【运行中】,线程运行中的状态
    // 线程的运行中状态是在JVM中执行中,但可能等待其他来自操作系统的资源,例如处理器。
    RUNNABLE,

    // 【阻塞】,线程阻塞等待监视器锁的线程状态。 
    // 处于阻塞状态的线程正在等待一个监视器锁进入 synchronized 块/方法 ,或通过调用 Object#wait(),重新进入 synchronized 块/方法。  
    BLOCKED,

    // 【无限期等待】,等待中的线程的状态
    // 以下方法可以使线程处于等待中的状态:
    // 1. Object#wait()
    // 2. Thread#join
    // 3. LockSupport#park()
    // 处于等待状态的线程正在等待另一个线程执行特定操作。
    // 例如,一个线程调用了 Object.wait() 在一个对象上,然后等待其他线程调用这个对象的 Object.notify() 或 Object.notifyAll()
    // 一个线程可以调用  Thread.join() 等待另一个线程执行完成。
    WAITING,

    // 【限期等待】,具有指定等待时间的等待线程的线程状态。
    // 由于以指定的正等待时间调用以下方法之一,线程处于定时等待状态:
    // 1. Thread#sleep
    // 2. Object#wait(long) 
    // 3. Thread#join
    // 4. LockSupport#parkNanos
    // 5. LockSupport#parkUntil
    TIMED_WAITING,

    // 【终止】,终止的线程的状态,线程已经执行完成
    TERMINATED;
}
  • New

    创建后,尚未启动运行(尚未调用start() 方法)。

  • Runnable

    正在运行,也可能是在等待 CPU 时间片。

  • Blocking

    等待一个监视器锁,而导致阻塞的线程状态。

  • Waiting

    无限期等待,等待其他线程显式通知唤醒。

    进入方法 退出方法
    Object#wait() Object.notify() / Object.notifyAll()
    Thread#join() 被调用的线程执行完毕
    LockSupport#park()
  • Timed Waiting

    限期等待,无需其他线程显式通知唤醒,在一定时间后会被系统自动唤醒。

    进入方法 退出方法
    Thread.sleep(time) 时间结束
    Object.wait(long) 时间结束 / Object.notify() / Object.notifyAll()
    Thread.join(long) 时间结束 / 被调用线程执行完成
    LockSupport.parkNanos()
    LockSupport.parkUntil()
  • Terminated

    线程执行完成后自动结束,或抛出异常导致提前结束。

它们的关系:

Java 多线程并发【2】线程
img

Java 中的线程使用

Java 中的线程提供了三种使用方式:

  • 实现 Runnable
  • 实现 Callable
  • 继承 Thread

前两者只是创建了线程执行的任务,然后需要通过创建 Thread 实例来执行。

它们的区别是 Callable 有返回值,返回值需要通过 FutureTask 进行封装:

public class MyCallable implements Callable<String> {
    public String call() {
        return "hi";
    }
}

public static void main(String[] args) throws ExecutionException, InterruptedException {
    MyCallable myCallable = new MyCallable();
    FutureTask<Integer> task = new FutureTask<>(myCallable);
    Thread thread = new Thread(task);
    thread.start();
    System.out.println(task.get());
}

常用的线程操作

sleep

使当前真正执行的线程休眠一段时间,可能会抛出 InterruptedException

yield

当前线程主动释放 CPU 资源。很少使用,对于调试或测试可能有用。

interrupted

线程在执行完成后会自动终止,如果遇到抛出异常的情况也会提前终止。Java 中提供了一个线程中断的异常:InterruptedException

interrupted() ,提前结束当前线程,调用该方法本质上会为线程设置一个中断标记,此时该方法返回为 true。

在实际的使用中,需要配合一个循环来实现提前结束的逻辑实现:

    private static class MyThread extends Thread {
        @Override
        public void run() {
            while (!interrupted()) {
                // ..
            }
            System.out.println("Thread end");
        }
    }
    // 实际使用
    thread2.start();
    thread2.interrupt();

线程之间的操作

Join

在 A 线程中调用 B 线程的 join() 方法会将 A 线程挂起,让 B 线程优先执行,并在 B 线程执行结束后,恢复 A 线程的执行。

wait

使下次进入无限期等待状态,等待指定条件满足时,恢复执行。

当条件满足时,需要手动调用 Object#notify()Object#notifyAll()显式通知线程恢复执行。

总结

本篇文章从深层次挖掘了线程的概念和在 Java 中的实现思路、到 Java 中具体的 API 。

  • 并发并不只是多线程,还可以是多进程。
  • 从硬件到软件,线程要经过内核线程、轻量级进程、用户线程三个层次。
  • 用户线程无需关心系统的实现,能够提供更大的并发和更好的性能。但只靠用户线程实现是很复杂的。
  • Java 中线程实际上是轻量级进程,需要内核线程的支持。
  • Kotlin 协程是一种用户线程 + 轻量级进程混合实现的方案。

线程调度方面:

  • 分为抢占式调度和协同式调度,协同式调度有严重的阻塞问题,一般都是抢占式的实现。


原文始发于微信公众号(八千里路山与海):Java 多线程并发【2】线程

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/85136.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!