什么,这就是多线程?

1,325 阅读7分钟

前言

相信大家多多少少都对线程有一些认知,也在平时的工作中使用过多线程。但我最近在越发的想要了解线程的前世今生,为什么它成为了八股文中浓墨重彩的一笔。所以今天就从这个角度来学习一下,什么TM叫多线程。

一个进程的诞生

在讨论线程前不得不提到的就是进程,因为在写这篇文章前,我也在网上查阅了大量的进程、线程的概念。感觉越看越糊涂,最后也对自己已有的认知产生了怀疑。所以这里再描述一下进程的概念,巩固自己的认知。
首先,做个思想实验。抛开具体的计算机原理,我们只要知道CPU是有序执行加载到内存中的指令的。假设现在磁盘上有个程序叫QQ音乐,这个程序里只有20条指令,CPU需要20秒的时间去执行完这些指令。那么在这20秒内,计算机无法做任何其他事情,这不是玩呢吗?我写博客的时候就不能听音乐,听音乐的时候就不能写博客?
所以,我们希望CPU能够同时执行多个程序,或者看起来同时执行多个程序。假设我又打开了新浪微博,我想一边刷微博一边听音乐,新浪微博这个程序有30条指令,我让CPU先执行一秒钟QQ音乐再执行一秒钟新浪微博。这样看上去就好像同时在执行多个程序。
好了,到此我们的思想实验结束。那么这个实验跟进程有什么关系呢?我的理解是:处理QQ音乐这20条指令的过程就是一个进程,处理新浪微博这30条指令的过程就是另外一个进程。
再给一个进程的抽象概念:进程是静态的程序的动态的执行过程。 到这里我大概对进程有了一个感性的认识。这就足矣,不再过多讨论。

一个线程的诞生

借助阮一峰老师举的例子,假定有一座工厂,工厂的电力有限每次只能给一个车间供电。也就是说一个车间开工的时候,其他车间都必须停工。这样进程就好比工厂的车间。
一个车间可以有很多工人,他们协同完成同一个任务。线程就好比车间里的工人。一个进程可以包括多个线程。
很简单,我们从这个例子中就有了一个比较整体的认知,到底什么是进程,什么是线程。

Java线程实现

有了以上的铺垫,我们就可以来学习一下java中线程的相关知识了。JDK中提供了Thread类Runnable接口来让我们实现自己的“线程”类。

  • 继承Thread类,并重写run方法;
  • 实现Runnable接口的run方法; 继承Thread类:
public class Demo {
    public static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("MyThread");
        }
    }

    public static void main(String[] args) {
        Thread myThread = new MyThread();
        myThread.start();
    }
}

当调用start()方法之后,线程才算成功启动。
实现Runnable接口:

public class Demo {
    public static class MyThread implements Runnable {
        @Override
        public void run() {
            System.out.println("MyThread");
        }
    }

    public static void main(String[] args) {

        new Thread(new MyThread()).start();

        // Java 8 函数式编程,可以省略MyThread类
        new Thread(() -> {
            System.out.println("Java 8 匿名内部类");
        }).start();
    }
}

Java线程状态

java线程中有6个状态:

// Thread.State 源码
public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}
  • 处于NEW状态的线程此时尚未启动。这里的尚未启动指的是还没调用Thread实例的start()方法。
  • RUNNABLE表示当前线程正在运行中。处于RUNNABLE状态的线程在Java虚拟机中运行,也有可能在等待CPU分配资源。
  • BLOCKED表示阻塞状态。处于BLOCKED状态的线程正等待锁的释放以进入同步区。
  • WAITING表示等待状态。处于等待状态的线程变成RUNNABLE状态需要其他线程唤醒。
  • TIMED_WAITING表示超时等待状态。线程等待一个具体的时间,时间到后会被自动唤醒。
  • TERMINATED终止状态。此时线程已执行完毕。

线程状态的转换

image.png

BLOCKED状态与RUNNABLE状态的转换

上面说过:处于BLOCKED状态的线程是在等待锁的释放。假如有a和b两个线程,a线程提前获得了锁且暂未释放锁,此时b线程就处于BLOCKED状态,我们先来看个示例:

@Test
public void blockedTest() {

    Thread a = new Thread(new Runnable() {
        @Override
        public void run() {
            testMethod();
        }
    }, "a");
    Thread b = new Thread(new Runnable() {
        @Override
        public void run() {
            testMethod();
        }
    }, "b");

    a.start();
    b.start();
    System.out.println(a.getName() + ":" + a.getState()); // 输出?
    System.out.println(b.getName() + ":" + b.getState()); // 输出?
}

// 同步方法争夺锁
private synchronized void testMethod() {
    try {
        Thread.sleep(2000L);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

可能有人会觉得a线程的状态一定是TIMED_WAITING,b线程的状态一定是BLOCKED状态。其实不然,在调用blockedTest()方法时,我们不要忽略还有一个main线程,main线程只保证a、b两个线程调用start()方法(转化为RUNNABLE状态),其他的就充满了不确定。可能打印出的两个线程状态都是RUNNABLE状态,可能其中某个线程是BLOCKED状态。
我们再来调整一下代码:

public void blockedTest() throws InterruptedException {
    ······
    a.start();
    Thread.sleep(1000L); // 需要注意这里main线程休眠了1000毫秒,而testMethod()里休眠了2000毫秒
    b.start();
    System.out.println(a.getName() + ":" + a.getState()); // 输出?
    System.out.println(b.getName() + ":" + b.getState()); // 输出?
}

在这个例子中两个线程的状态转换如下

  • a的状态转换过程:RUNNABLE(a.start()) -> TIMED_WATING(Thread.sleep())->RUNABLE(sleep()时间到)->BLOCKED(未抢到锁)  -> TERMINATED
  • b的状态转换过程:RUNNABLE(b.start()) -> BLOCKED(未抢到锁)  ->TERMINATED 斜体表示可能出现的状态, 大家可以在自己的电脑上多试几次看看输出。同样,这里的输出也可能有多钟结果。

WAITING状态与RUNNABLE状态的转换

根据转换图我们知道有3个方法可以使线程从RUNNABLE状态转为WAITING状态。我们主要介绍下Object.wait()Thread.join()

Object.wait()

调用wait()方法前线程必须持有对象的锁。

线程调用wait()方法时,会释放当前的锁,直到有其他线程调用notify()/notifyAll()方法唤醒等待锁的线程。

需要注意的是,其他线程调用notify()方法只会唤醒单个等待锁的线程,如有有多个线程都在等待这个锁的话不一定会唤醒到之前调用wait()方法的线程。

同样,调用notifyAll()方法唤醒所有等待锁的线程之后,也不一定会马上把时间片分给刚才放弃锁的那个线程,具体要看系统的调度。

Thread.join()

调用join()方法,会一直等待这个线程执行完毕(转换为TERMINATED状态)。

我们再把上面的例子线程启动那里改变一下:

public void blockedTest() {
    ······
    a.start();
    a.join();
    b.start();
    System.out.println(a.getName() + ":" + a.getState()); // 输出 TERMINATED
    System.out.println(b.getName() + ":" + b.getState());
}

要是没有调用join方法,main线程不管a线程是否执行完毕都会继续往下走。
a线程启动之后马上调用了join方法,这里main线程就会等到a线程执行完毕,所以这里a线程打印的状态固定是TERMINATED
至于b线程的状态,有可能打印RUNNABLE(尚未进入同步方法),也有可能打印TIMED_WAITING(进入了同步方法)。

TIMED_WAITING与RUNNABLE状态转换

TIMED_WAITING与WAITING状态类似,只是TIMED_WAITING状态等待的时间是指定的。具体过程就不再赘述。

结语

以上差不多就是java线程的基础概念了,为了能够简单轻松的了解线程的相关知识,没有对实现原理做过多深入的理解。以此达到先了解再深入的循序渐进的学习方法。