# 线程

本篇讲解 Java 的线程基础。

谈起线程的状态(生命周期),操作系统层面上和 Java 层面上是不同的:

  • 操作系统:初始状态(NEW),可运行状态(READY),运行状态(RUNNING),等待(WAITING),终止状态(TERMINATED)。

  • Java:新建(New),可运行(Runable),阻塞(Blocking),无限等待(Waiting),限时等待(Timed Waiting),死亡(Terminated)。

在操作系统篇中,我们提到的拿不到锁休眠(阻塞),等待条件变量进入等待队列休眠,在操作系统层面都是 WAITING。

注意:

  • Java 没有 Running 状态,Runnable 就包括了 Running 和 Ready 以及部分 wait。
  • 阻塞和等待的区别在于,阻塞是被动的,线程拿不到锁就会阻塞,但是等待是主动的,通过调用 sleepwait 实现。

# 传入任务

首先要构建一个线程,在 Thread 中的 init(..) 方法,会将当前线程设置为父线程,新线程是由父线程进行空间分配,子线程继承了父线程是否为 Daemon ,优先级,可继承的 ThreadLocal 等。

在创建线程对象时,可以传入 Runnable 或者 Callable ,或者继承 Thread ,重写 run 方法。但是实现接口比继承好得多。

new Thread(()->System.out.println("hello world")).start();

# 基础机制

  • 守护线程:当所有非守护线程结束后,程序也就会终止,同时杀死所有守护线程。使用 setDaemon(true) 将线程设置为守护线程。如果守护线程中有 finally ,并且在执行 finally 前所有的非守护线程都结束了,虚拟机就会停止,不会执行 finally 语句

  • sleep() :休眠当前线程,如果该线程持有锁,不会释放锁

  • yield() :如果当前线程已经完成生命周期最重要的部分,可以切换给其他线程执行。该方法只是给调度器一个建议。

# 线程中断

中断理解为线程的一个标识位属性,表示一个运行的线程是否被其他线程进行了中断操作,打上中断标识不会立即中断线程,而是需要线程自己来处理这个中断标识。

一个线程执行完毕会自动结束,如果运行过程中发生异常也会提前结束。有时在外部我们就是希望中断某一个线程,在 Java 中专门设计了一个 InterruptedException 来提前结束线程。

如果线程处于阻塞,限期等待或者无限期等待,此时调用 interrupt() 就会触发异常从而结束线程。但是不能中断 I/O 阻塞synchronized 锁阻塞。

@Override
public void run() {
    try {
        Thread.sleep(8000);
        System.out.println("Thread run");
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
thread1.start();
Thread.sleep(1000);
thread1.interrupt();

当一个线程内部是一个循环,且没有执行 sleep 等会抛出中断异常的操作,而我们又希望在外部控制线程中断,可以使用 interrupted() 方法:

// 这种设计限制还是比较大的
public void run() {
    while(!interrupted()) {
        // ..
    }
}

注意:

  • sleep() 如果要抛出中断异常,会先清空中断标记,再抛出异常。

  • interrupted 会清空中断标记,而 isInterrupted() 不会。

  • 如果一个线程处于终结状态, isInterrupted 也返回 false

# 线程通信

  • 使用 volatilesynchronized 两个关键字,前者保证共享变量的实时更新,保证了可见性,底层原理是总线嗅探机制;后者可以实现同步代码块,保证同一时间只能有一个线程处于同步代码块,保证线程对变量访问的可见性和排他性。

需要注意的是,之前的例子,多个线程将 count 变量累加 10000,不能用 volatile 修饰 count 来保证线程安全。该关键字不保证线程安全。假设一下,在多 CPU 里面,已经有两个线程将要同时执行关于 count 的写入指令, volatile 对此是无法避免的。

  • 还有一种通信方式就是等待 / 通知机制:该部分类似于之前讲的条件变量那部分的内容,父线程等待子线程结果(Java 中也可以使用 join 方法)。
    • wait() :进入等待队列,线程放弃锁和时间片,满足某条件,才会被唤醒( notify/notifyAll )。在之前的操作系统篇提到, wait 应该在获得锁后才允许被调用wait() 还有两个重载函数,传入参数为等待时间。
    • notify/notifyAll() :通知等待队列中的线程从 wait 返回,后者则是将队列中所有线程都唤醒。全部唤醒,还记得覆盖条件的概念吗?

其实所谓的唤醒,是将等待队列中的线程放到了同步队列中,因为线程调用了 notify/notifyAll 后还没有释放锁。

  • 管道输入 / 输出流:使用 PipedOutputStream/PipedInputStream/PipedReader/PipedWriter
// 管道输入输出主要用于线程之间的数据传输
// 下面代码改编自《Java 并发编程的艺术》,书上用的是字符流,但是中文会出现乱码,所以我改成字节流了
public class Main {
    public static void main(String[] args) throws Exception {
        PipedOutputStream out = new PipedOutputStream();
        PipedInputStream in = new PipedInputStream();
        // 输入输出流连接,不然会抛出 IOException
        out.connect(in);
        Thread printThread = new Thread(new Print(in), "PrintThread");
        printThread.start();
        int receive = 0;
        byte[] buf = new byte[100];
        
        try {
            while((receive = System.in.read(buf)) != -1) {
                out.write(buf,0 ,receive);
            }
        } finally {
            out.close();
        }
    }
    static class Print implements Runnable {
        private PipedInputStream in;
        byte[] buf = new byte[100];
        public Print(PipedInputStream in) {
            this.in = in;
        }
        public void run() {
            int receive = 0;
            try {
                while (-1 != (receive = in.read(buf))) {
                    System.out.print(new String(buf, 0, receive));
                }
            } catch (IOException ignored) {
            }
        }
    }
}
  • ThreadLocal :该类到时候会详细讲解,此处简单介绍。它是线程变量,存储的是一个键值对,以 ThreadLocal 对象为键,存储指定的泛型值。每个线程都有一个 ThreadLocalMap ,存放的就是各种 ThreadLocal->value 对。
public class ThreadLocalTest {
    private static ThreadLocal<String> threadLocal = new ThreadLocal<String>();
    
    public static void main(String[] args){
        // 创建第一个线程
        Thread threadA = new Thread(()->{
            threadLocal.set("ThreadA:" + Thread.currentThread().getName());
            System.out.println("线程A本地变量中的值为:" + threadLocal.get());
        },"ThreadA");
        
        // 创建第二个线程
        Thread threadB = new Thread(()->{
            threadLocal.set("ThreadB:" + Thread.currentThread().getName());
            System.out.println("线程B本地变量中的值为:" + threadLocal.get());
        },"ThreadB");
        
        // 启动线程 A 和线程 B
        threadA.start();
        threadB.start();
    }
}
// 每次的打印结果可能不一致
// 线程 B 本地变量中的值为:ThreadB:ThreadA
// 线程 A 本地变量中的值为:ThreadA:ThreadB

如果你此时对 ThreadLocal 还不熟悉(很正常),之后会有一篇文章专门讲解这个,当然,看源码也是必不可缺的。

# 参考

https://pdai.tech/md/java/thread/java-thread-x-thread-basic.html

《Java 并发编程的艺术》第四章