Java 多线程基础与 Thread 类详解

1 基本概念

1.1 程序、进程和线程的概念

在计算机科学中,程序是一组指令的有序集合,用于告诉计算机如何执行特定任务。程序通常由开发人员使用编程语言编写,并在执行之前需要被翻译或解释为计算机可以理解的形式。程序可以用于完成各种任务,从简单的数学计算到复杂的应用程序。

进程是指计算机系统中正在运行的程序的实例。在操作系统中,每个进程都有自己的内存空间和资源分配,包括处理器时间、文件和输入/输出设备。每个进程都是相互独立的,它们在内存中运行,并按照特定的顺序执行指令。进程可以并发地执行,这意味着多个进程可以同时运行,但实际上它们在处理器上以快速切换的方式交替执行。进程还可以拥有子进程,这些子进程是由父进程创建的。父进程可以控制子进程的执行,并通过进程间通信机制与其进行交互。

线程是进程内部的执行单位,它是进程中的一个实体,可以独立执行。一个进程可以包含多个线程,这些线程共享进程的资源,如内存空间、文件和其他系统资源。与进程相比,线程更加轻量级,创建和销毁线程的开销相对较小。线程之间的切换也比进程之间的切换更加高效。因此,线程被广泛用于实现并发和并行的编程模型,以提高程序的性能和效率。

在一个进程内部,所有线程共享相同的地址空间,这意味着它们可以相互访问相同的变量和数据结构。这也使得线程之间的通信和数据共享更加方便和高效。然而,由于线程共享相同的资源,因此需要特殊的同步机制来确保多个线程之间的正确协调和避免竞争条件。

线程可以分为两种类型:用户线程和内核线程。用户线程是在用户空间中创建和管理的,由应用程序自己负责调度。内核线程是由操作系统内核创建和管理的,操作系统负责线程的调度和执行。

1.2 线程的生命周期

线程的生命周期可以被描述为 NEW、RUNNABLE、RUNNING、BLOCKED、TERMINATED 五个状态:

线程生命周期

1.2.1 线程的 NEW(新建) 状态

当线程对象被创建但还没有调用 start() 方法时,线程处于 NEW 状态。在这个阶段,线程被创建并分配了必要的系统资源,但尚未开始执行。

1.2.2 线程的 RUNNABLE(可运行) 状态

一旦调用了线程的 start() 方法,线程就进入了 RUNNABLE 状态。在 RUNNABLE 状态下,线程等待操作系统的线程调度器来调度,并分配处理器时间给它。具体而言,线程等待 CPU 时间片的分配,以便执行其任务。

1.2.3 线程的 RUNNING(运行) 状态

当线程获得 CPU 时间片并开始执行时,线程处于 RUNNING 状态。在 RUNNING 状态下,线程执行其任务代码,执行相关操作。

当线程处于 RUNNING 状态时,可以发生以下状态转换:

  1. 进入阻塞状态 (BLOCKED)
    • 调用了 sleep()wait() 方法;
    • 进行了某个阻塞 I/O 操作,然后被阻塞;
    • 获取排他锁资源失败,进而被加入等待队列;
  2. 恢复到可运行状态 (RUNNABLE)
    • CPU 调度器使该线程放弃执行;
    • 线程主动调用 yield 方法,放弃 CPU 执行权;
  3. 线程终止 (TERMINATED)
    • 调用了 stop() 方法(虽然不推荐这么做);
    • 某个退出线程逻辑生效;

1.2.4 线程的 BLOCKED(阻塞) 状态

在阻塞状态下,线程将暂时停止执行,直到特定的条件得到满足。

当线程处于 BLOCKED 状态时,可以发生以下状态转换:

  1. 解除阻塞进入可运行状态 (RUNNABLE)
    • 阻塞 I/O 中读取到了想要的字节流;
    • sleep 中的线程完成了指定时间的休眠;
    • wait 中的线程被其他线程 notify / notifyAll 唤醒;
    • 线程获取到了某个锁资源;
    • 线程阻塞过程中被中断信号打断;
  2. 直接进入终止 (TERMINATED) 状态
    • 调用了 stop() 方法(虽然不推荐这么做);
    • 线程意外死亡 (JVM Crash)。

1.2.5 线程的 TERMINATED(终止) 状态

线程的生命周期最终以终止状态结束。线程可以因为成功完成了它的任务或者因为出现了异常而终止。无论是哪种情况,一旦线程终止,它将释放所占用的资源,并不再被调度执行。终止的线程不再参与进一步的指令执行和并发调度,其执行上下文和内存空间将被回收和清理。

2 Thread 源码解析

2.1 Thread 构造方法解析

Java 中的 Thread 为我们提供了比较丰富的构造方法。

2.1.1 线程的命名

在构造线程的时候可以为线程取一个特殊意义的名字,尤其是在线程比较多的场景中,为线程赋予包含特殊含义的名称更有助于问题的排查和跟踪。

  • 线程的默认命名

    Thread 类中,以下几个构造方法为线程提供了默认的命名规则:

    1
    2
    3
    
    public Thread()
    public Thread(Runnable target)
    public Thread(ThreadGroup group, Runnable target)

    Thread(Runnable target) 为例,源码实现如下:

    1
    2
    3
    
    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

    如果没有为线程显式的指定名称,那么线程将会以 Thread- 为前缀与一个自增数字进行组合,这个自增数字在整个 JVM 进程中将不断自增:

    1
    2
    3
    4
    5
    
    // static 字段,JVM 进程中唯一
    private static int threadInitNumber;
    private static synchronized int nextThreadNum() {
        return threadInitNumber++;
    }
  • 线程的自定义命名

    Thread 类中,以下几个构造方法支持为线程自定义名称:

    1
    2
    3
    4
    5
    6
    
    public Thread(String name)
    public Thread(ThreadGroup group, String name)
    public Thread(Runnable target, String name)
    public Thread(ThreadGroup group, Runnable target, String name)
    public Thread(ThreadGroup group, Runnable target, String name,
                    long stackSize)
  • 修改线程名称

    无论是使用默认的线程命名规则还是自定义线程名称,在线程启动之前,还有一个机会可以对其进行修改,即调用 setName 方法。源码如下:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    public final synchronized void setName(String name) {
        checkAccess();
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }
        // 更新线程名称
        this.name = name;
        if (threadStatus != 0) {
            // 如果线程状态不为 NEW,则更改不会生效
            setNativeName(name);
        }
    }

    需要注意的是,一旦线程启动,其名称就无法再进行修改。

2.1.2 线程的父子关系

Thread 的所有构造方法,最终都会调用 init 方法,我们截取其代码片段进行分析,不难发现,任何线程都会有一个父线程:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
private void init(ThreadGroup g, Runnable target, String name,
                    long stackSize, AccessControlContext acc,
                    boolean inheritThreadLocals) {
    if (name == null) {
        throw new NullPointerException("name cannot be null");
    }
    // 设置线程名称
    this.name = name;
    // 获取创建当前 Thread 的线程作为父线程
    Thread parent = currentThread();
    SecurityManager security = System.getSecurityManager();
    
    // ...
}

在上述代码中,currentThread() 方法用于获取当前创建 Thread 对象的线程。这是因为在该代码段执行时,Thread 对象的状态为 NEW,即表示该线程还未被创建。因此,currentThread() 方法返回的是创建该 Thread 对象的线程实例。

因此我们可以得出以下结论:

  • 一个线程肯定是由另一个线程创建的;
  • 被创建线程的父线程是创建它的线程。

2.1.3 ThreadGroup

Thread 的构造方法可以显式的指定线程的 Group,即 ThreadGroup 类,我们接着看 init 方法中的片段:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
    //...
    if (g == null) {
        if (security != null) {
            g = security.getThreadGroup();
        }
        if (g == null) {
            g = parent.getThreadGroup();
        }
    }
    //...
    // 如果参数设置了,则显式赋值
    this.group = g;
    //...
}

可见,如果构造 Thread 的时候没有显式的指定 ThreadGroup,那么子线程将会被加入父线程所在的线程组。

2.1.4 Runnable

Thread 的构造方法可以显式的指定线程的 Runnable,也可以传入 null。

1
2
3
4
5
6
7
private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
    //...
    this.target = target;
    //...
}

2.2 线程状态枚举类 State

上文我们介绍了线程的五种状态,这些状态通过 Thread 类内部的一个枚举类来表示。源码如下:

1
2
3
4
5
6
7
8
public enum State {
    NEW, // 0
    RUNNABLE, // 1
    BLOCKED, // 2
    WAITING, // 3
    TIMED_WAITING, // 4
    TERMINATED; // 5
}

2.3 start() 方法解析

Thread#start 源码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public synchronized void start() {
    /**
     * 如果线程状态不为 NEW,抛出异常
     * 所以 start() 方法不可以多次调用
     */
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    /* 通知组,该线程即将启动 */
    group.add(this);

    boolean started = false;
    try {
        // 核心 native 方法调用
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* 忽略异常 */
        }
    }
}

start 的方法很简单,最核心的部分就是 native 方法 start0,这是一个 JNI 接口:

1
private native void start0();

当 Java 程序调用 start0 接口后,JVM 会在 start0 的 C++ 实现中调用该线程的 run 方法。

2.4 Thread API 详解

2.4.1 sleep()

sleep 是一个静态方法,它有两个重载方法,源码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public static native void sleep(long millis) throws InterruptedException;
public static void sleep(long millis, int nanos)
    throws InterruptedException {
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }
        // nanos 并不精确处理,符合条件后,将 ms 加 1
        if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
            millis++;
        }
        // 调用毫秒 native 方法
        sleep(millis);
    }

sleep() 方法会使当前线程进入毫秒级休眠,虽然可以指定时间,但是最终要以系统的定时器和调度器的精度为准。

2.4.2 yield()

yield() 是一个启发式方法,它会提醒调度器当前线程愿意放弃 CPU 资源,如果 CPU 资源不紧张,则会忽略这种提醒。

1
public static native void yield();

2.4.3 线程优先级 priority

线程拥有优先级的概念,理论上优先级越高的线程越容易被 CPU 调度,但事实上不一定。

1
2
3
4
// 设置优先级
public final void setPriority(int newPriority)
// 获取优先级
public final int getPriority()

设置线程的优先级是一个提醒操作:

  • 对于 root 用户,它会提醒操作系统我们想要设置的优先级别,否则忽略提醒;
  • 如果 CPU 比较繁忙,则会考虑 root 的优先级设置,否则几乎不考虑线程优先级。

因此,不推荐在程序设计中企图通过线程优先级来实现某些业务。

2.4.4 getId()

getId() 方法用于获取线程的唯一 ID,线程的 ID 在整个 JVM 进程中唯一,并且从 0 开始递增。源码如下:

1
2
3
public long getId() {
    return tid;
}

2.4.5 currentThread()

currentThread() 方法用于返回当前执行线程的引用,它是一个 native 方法,源码如下:

1
public static native Thread currentThread();

2.4.6 上下文类加载器 ContextClassLoader

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
 * 获取线程上下文的类加载器,简单来说就是这个线程是由哪个类加载的。
 *  默认情况下与父线程保持相同的类加载器
 */
@CallerSensitive
public ClassLoader getContextClassLoader() {
    if (contextClassLoader == null)
        return null;
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        ClassLoader.checkClassLoaderPermission(contextClassLoader,
                                                Reflection.getCallerClass());
    }
    return contextClassLoader;
}
/**
 * 设置类加载器,这个方法可以打破 Java 类加载器的双亲委派机制,算是一个后门
 */
public void setContextClassLoader(ClassLoader cl) {
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        sm.checkPermission(new RuntimePermission("setContextClassLoader"));
    }
    contextClassLoader = cl;
}

2.4.7 线程中断 interrupt

线程的 interrupt 是一个非常重要的 API,与中断相关的 API 有以下几个:

1
2
3
public void interrupt()
public static boolean interrupted()
public boolean isInterrupted()
  • isInterrupted():

    isInterrupted() 用于判断当前线程是否被中断,该方法仅仅判断一下中断标识的状态,不会对标识产生任何影响。

  • interrupted()

    interruptedThread 类中的一个静态方法,虽然也用于判断当前线程的中断状态,但该方法会直接擦除线程的中断标识。

    如果一个线程被打断了,那么第一次调用 interrupted() 方法会返回 true,并清除中断标识;后续再调用这个方法会永远返回 false,除非在此期间线程又一次被打断。

  • interrupt():

    当调用 Object.wait(...)Thread.sleep(...)Thread.join(...)InterruptibleChannel 的 I/O 操作、Selector.wakeup() 等方法时,线程会进入阻塞状态。此若另外一个线程调用当前线程的 interrupt() 方法,则会打断这种阻塞状态。一旦线程在阻塞状态下被打断,则会抛出一个 InterruptedException 异常。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    public void interrupt() {
        if (this != Thread.currentThread()) {
            checkAccess();
    
            // thread may be blocked in an I/O operation
            synchronized (blockerLock) {
                Interruptible b = blocker;
                if (b != null) {
                    interrupt0();  // set interrupt status
                    b.interrupt(this);
                    return;
                }
            }
        }
    
        // set interrupt status
        interrupt0();
    }
    1
    
    private native void interrupt0();

    interrupt() 的核心是 native 方法 interrupt0(),其 JNI 接口在 Thread.c 里定义如下:

    1
    2
    3
    4
    
    static JNINativeMethod methods[] = {
        // interrupt0() 方法调用到 native 层的 JVM_Interrupt() 方法
        {"interrupt0",       "()V",        (void *)&JVM_Interrupt},
    };

    然后 JVM_Interrupt 在 jvm.cpp 中的实现如下:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    JVM_ENTRY(void, JVM_Interrupt(JNIEnv* env, jobject jthread))
        JVMWrapper("JVM_Interrupt");
    
        // Ensure that the C++ Thread and OSThread structures aren't freed before we operate
        oop java_thread = JNIHandles::resolve_non_null(jthread);
        MutexLockerEx ml(thread->threadObj() == java_thread ? NULL : Threads_lock);
        // We need to re-resolve the java_thread, since a GC might have happened during the
        // acquire of the lock
        JavaThread* thr = java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread));
        if (thr != NULL) {
            //调用 Thread.cpp 里的函数
            Thread::interrupt(thr);
        }
    JVM_END

    其中,Thread.cpp 中的 interrupt(thr) 实现如下:

    1
    2
    3
    4
    5
    
    void Thread::interrupt(Thread* thread) {
        trace("interrupt", thread);
        debug_only(check_for_dangling_thread_pointer(thread);)
        os::interrupt(thread);
    }

    可见,最终执行的是 os::interrupt(thread); 以 Linux 平台的 os_linux.cpp 为例:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    
    void os::interrupt(Thread* thread) {
        assert(Thread::current() == thread || Threads_lock->owned_by_self(),
            "possibility of dangling Thread pointer");
    
        OSThread* osthread = thread->osthread();
    
        // 如果中断标记为 false(防止 sleep 无法再次休眠)
        if (!osthread->interrupted()) {
            //  设置中断标记位为 true
            osthread->set_interrupted(true);
            // More than one thread can get here with the same value of osthread,
            // resulting in multiple notifications.  We do, however, want the store
            // to interrupted() to be visible to other threads before we execute unpark().
            OrderAccess::fence();
            ParkEvent * const slp = thread->_SleepEvent ;
            //唤醒线程,对应 sleep 唤醒
            if (slp != NULL) slp->unpark() ;
        }
    
        //唤醒线程,对应 wait/join 操作唤醒等
        if (thread->is_Java_thread())
            ((JavaThread*)thread)->parker()->unpark();
    
        //唤醒线程,对应 synchronized 获取锁挂起
        ParkEvent * ev = thread->_ParkEvent ;
        if (ev != NULL) ev->unpark() ;
    }

    因此我们可以得出结论,在 Java 层调用 Thread.interrupt() 方法,最终底层完成了两件事:

    1. 将中断标记设置为 true。
    2. 将挂起的线程唤醒。

2.4.8 join()

Thread 中的 join() 是一个非常重要的方法,使用它的特性可以实现很多强大的功能,它与 sleep() 一样都是可中断方法。Thread 的 API 为我们提供了以下三个重载方法:

1
2
3
4
5
6
7
public final void join() throws InterruptedException

public final synchronized void join(long millis) 
    throws InterruptedException

public final synchronized void join(long millis, int nanos)
    throws InterruptedException

如果当前线程 join 线程 A,则会让当前线程进入等待,直到线程 A 的生命周期结束,或者到达指定时间。

3 线程的优雅关闭

通常情况下,我们不会手动终止一个线程,而是允许线程执行完毕,然后自然停止。然而,在某些特殊情况下,我们需要提前停止线程,例如:当用户突然关闭程序或程序运行出错需要重新启动时。

对于 Java 来说,最合适的线程停止方式是使用中断。然而,中断仅仅起到通知被停止线程的作用。对于被停止的线程而言,它拥有完全的自主权,可以选择立即停止,也可以选择延迟一段时间后停止,甚至可以选择不停止。因此线程执行时,应该定期检查中断标记,如果中断标记被设置成 true,就说明有程序想终止该线程,进而退出线程。

那么为什么 Java 不提供强制停止线程的能力:

Java 希望程序间能够相互通知、相互协作地管理线程,因为如果不了解对方正在做的工作,贸然强制停止线程就可能会造成一些安全的问题,为了避免造成问题就需要给对方一定的时间来整理收尾工作。

以下是线程优雅关闭的最佳实践:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public void run() {
	while (!Thread.currentThread().isInterrupted()) {
        try {
            doSomething1();
            doSomething2();
        } catch (Exception e) {
            if (e instanceof InterruptedException) {
                Thread.currentThread().interrupt();
            }
        }
	}
}

一个常见的误区是,既然是使用标记,那么能否使用 volatile 字段作为这个标记?

答案是不能: volatile 字段虽然可以保证实时的可见性,但是其不具备唤醒阻塞状态的能力,因而无法代替中断。


欢迎关注我的公众号,第一时间获取文章更新:

微信公众号

相关内容