专栏原创出处:github-源笔记文件 github-源码 ,欢迎 Star,转载请附上原文出处链接和本声明。

Java 并发编程专栏系列笔记,系统性学习可访问个人复盘笔记-技术博客 Java 并发编程

# 什么是线程

什么是进程:现代操作系统在运行一个程序时,会为其创建一个进程。例如,启动一个 Java 程序,操作系统就会创建一个 Java 进程。

什么是线程:现代操作系统调度的最小单元是线程,也叫轻量级进程,在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。

# 为什么要用线程

  • 更多的处理器核心(利用处理器高频多核心优势)
  • 更快的响应时间(并行计算)
  • 更好的编程模型(方便开发人员编程)

# 线程优先级

现代操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当线程的时间片用完了就会发生线程调度,并等待着下次分配。线程分配到的时间片多少也就决定了线程使用处理器资源的多少,而线程优先级就是决定线程需要多或者少分配一些处理器资源的线程属性。  

  1. 如何控制优先级?

通过 setPriority(int) 方法来修改优先级,默认优先级是 5(范围从 1~10),优先级高的线程分配时间片的数量要多于优先级低的线程。

  1. 设置线程优先级时考虑情况:
  • 频繁阻塞 (休眠或者 I/O 操作) 的线程需要设置较高优先级
  • 偏重计算 (需要较多 CPU 时间或者偏运算) 的线程则设置较低的优先级,确保处理器不会被独占
  1. 注意

线程优先级不能作为程序正确性的依赖,因为操作系统可以完全不用理会 Java 线程对于优先级的设定。

# 线程状态

  • 初始状态(NEW)

尚未启动的线程处于此状态。

  • 就绪状态(除CPU之外,其它的运行所需资源都已全部获得)

    • 调用线程的 start 方法,此线程进入就绪状态。
    • 当前线程 sleep 方法结束,其他线程 join 结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。
    • 当前线程时间片用完了,调用当前线程的 yield 方法,当前线程进入就绪状态。
    • 锁池里的线程拿到对象锁后,进入就绪状态。
  • 运行中状态(RUNNABLE)

线程调度程序从可运行池中选择一个线程作为当前线程时,当前线程线程所处的状态就是运行状态。(JDK 把就绪状态归于 RUNNABLE 状态)

  • 阻塞状态(BLOCKED)

阻塞状态是线程阻塞在等待监视器的状态。

  • 等待(WAITING)

处于这种状态的线程不会被分配 CPU 执行时间,它们要等待被显式地做出一些动作(通知或者中断),否则会处于无限期等待的状态。

  • 超时等待(TIMED_WAITING)

处于这种状态的线程不会被分配 CPU 执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。

  • 终止状态(TERMINATED)

当线程的 run 方法完成时,或者主线程的 main 方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。 在一个终止的线程上调用 start 方法,会抛出 java.lang.IllegalThreadStateException 异常。


线程状态变迁

# Daemon 线程

Daemon 线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。当一个 Java 虚拟机中不存在非 Daemon 线程的时候,Java 虚拟机将会退出。可以通过调用 Thread.setDaemon(true) 将线程设置为 Daemon 线程。

WARNING

  • Daemon 属性需要在启动线程之前设置,不能在启动线程之后设置
  • Java 虚拟机退出时 Daemon 线程中的 finally 块并不一定会执行 (Java 虚拟机中的所有 Daemon 线程都需要立即终止)
  • 不能依靠 finally 块中的内容来确保执行关闭或清理资源

# 线程的初始化、启动、中断、停止

# 初始化线程(init)

线程类 Thread 构造函数最终调用 init 方法如下:

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 parent = currentThread(); // 当前线程就是该线程的父线程

        g.addUnstarted();

        this.group = g;  // 设置所属线程组
        this.daemon = parent.isDaemon(); // 将 daemon 属性设置为父线程的对应属性
        this.priority = parent.getPriority(); // 将 priority 属性设置为父线程的对应属性
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        this.target = target;
        setPriority(priority);
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        this.stackSize = stackSize; // 存放指定的堆栈大小,零表示该参数将被忽略

        tid = nextThreadID(); // 分配一个线程 ID
    }

# 启动线程(start)

线程对象在初始化完成之后,调用 start 方法就可以启动这个线程。

线程 start 方法的含义是:当前线程 (即 parent 线程) 同步告知 Java 虚拟机,只要线程规划器空闲,应立即启动调用 start 方法的线程。

public class Thread implements Runnable , Runnable 接口中只有一个 run 方法。一个类实现 Runnable 接口后,并不代表该类是一个“线程”类,不能直接运行,必须通过 Thread 实例才能创建并运行线程。

Thread.start 方法源码:

    public synchronized void start() {
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

    private native void start0(); // 最终底层调用启动线程

# 中断线程(interrupt)

中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。中断好比其他线程对该线程打了个招呼,其他线程通过调用该线程的 interrupt 方法对其进行中断操作。 线程通过检查自身是否被中断来进行响应,线程通过方法 isInterrupted 来进行判断是否被中断,也可以调用静态方法 Thread.interrupted 对当前线程的中断标识位进行复位。如果该线程已经处于终结状态,即使该线程被中断过,在调用该线程对象的 isInterrupted 时依旧会返回 false。

  1. 中断状态设置情况:
  • 如果该线程调用 Object.wait 被阻塞,或者调用 Thread.join , Thread.sleep 方法,那么它的中断状态将被清除,同时抛出 InterruptedException
  • 如果该线程被阻塞在 I/O 操作 InterruptibleChannel ,则信道被关闭,该线程设置为中断状态,同时抛出 java.nio.channels.ClosedByInterruptException
  • 如果这个线程被阻塞在 java.nio.channels.Selector,该线程设置为中断状态
  • 许多声明抛出 InterruptedException 的方法 (例如 Thread.sleep(long millis) 方法) 这些方法在抛出 InterruptedException 之前,Java 虚拟机会先将该线程的中断标识位清除,然后抛出 InterruptedException,此时调用 isInterrupted 方法将会返回 false
  • 中断一个不存在的线程不会有任何效果
  1. 为什么需要中断
  • 线程运行过程中,我们根据实际需求需要中断线程执行
  • 处理器等其他外界条件让线程不能正常运行下去
  1. 中断操作会让线程停止吗? 不会,中断仅仅是一个状态值,可能会抛出异常(参考中断状态设置情况)

# 停止线程

  • 线程运行代码中设置标识符,通过标识符判断退出
  • 调用中断方法,代码逻辑中判断是否中断或者捕获中断相关异常后退出

# 过期的 suspend、resume 和 stop

suspend、resume 和 stop 方法分别致使线程暂停、恢复和终止工作

不建议使用的原因主要有:

  • suspend/resume 方法,在调用后,线程不会释放已经占有的资源 (比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。
  • stop 方法,在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。

注意:
正因为 suspend、resume 和 stop 方法带来的副作用,这些方法才被标注为不建议使用的过期方法,而暂停和恢复操作可以用后面提到的 《线程等待通知机制》 来替代。

详细原因可参考 Why are Thread.stop, Thread.suspend and Thread.resume Deprecated?

# 线程 join

如果一个线程 A 执行了 thread.join 语句,其含义是:当前线程 A 等待 thread 线程终止之后才从 thread.join 返回。

线程 Thread 除了提供 join 方法之外,还提供了 join(long millis) 和 join(long millis,int nanos) 两个具备超时特性的方法。这两个超时方法表示,如果线程 thread 在给定的超时时间里没有终止,那么将会从该超时方法中返回。

  • join 方法使用示例
    • 模拟了 3 个线程,每个线程中引用了前一个线程(previous)。
    • 在 run 方法中执行previous.join() 表示 previous 线程执行完成后才可以执行 previous.join() 后的逻辑。
    public static void main(String[] args) throws InterruptedException {
        Thread previous = Thread.currentThread();
        for (int i = 0; i < 3; i++) {
            // 每个线程拥有前一个线程的引用,需要等待前一个线程终止,才能从等待中返回
            final Thread thread = new Thread(new Domino(previous), "t-" + i);
            thread.start();
            previous = thread;
        }
        TimeUnit.SECONDS.sleep(5);
        System.out.println(Thread.currentThread().getName() + " terminate.");
    }

    static class Domino implements Runnable {
        private Thread previous; // 当前线程的前一个线程引用

        public Domino(Thread previous) {
            this.previous = previous;
        }

        @Override
        public void run() {
            final String name = Thread.currentThread().getName();
            try {
                System.out.println(name + " before .join()"); // join 方法调用前逻辑
                previous.join();
                System.out.println(name + " after .join()"); // join 方法调后前逻辑
            } catch (InterruptedException e) {
            }
            System.out.println(name + " terminate.");
        }
    }
    /*
    最终打印
    
    t-0 before .join()
    t-2 before .join()
    t-1 before .join()
    main terminate.
    t-0 after .join()
    t-0 terminate.
    t-1 after .join()
    t-1 terminate.
    t-2 after .join()
    t-2 terminate.
     */
  • 思考:

happens-before 语义中的 join 规则?遗忘请复习《内存模型-基础概念》中 happens-before 部分。

  • 什么时候用 join?

线程之间存在一种等待关系时使用。比如 A 线程必须等 B 线程执行后才可以执行 join 后逻辑。

  • join 怎么实现的?

参考 Thread.join 源码分析可知实现使用了 wait 机制,即 《线程等待通知机制》

    public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

# 线程通信

  • volatile 和 synchronized 关键字
  • 等待通知机制
  • ThreadLocal

# 总结思考

  • 启动线程为什么调用 start 而不是 run ?

# 参考

最后修改时间: 2/17/2020, 4:43:04 AM