写于:2019-11-01

线程存在,就是为了让程序能够并行的执行。并行指的就是一系列任务在计算中同时运行的行为。如:浏览网页的同时播放音乐等。

一、线程简介

1、概念

wiki百科-线程

2、一个简单的线程案例

模拟:同时浏览网页,同时听歌。

代码

  1. public class SimpleThread {
  2. public static void main(String[] args) {
  3. new Thread(()->{
  4. while (true){
  5. System.out.println("======听音乐======");
  6. sleep(1);
  7. }
  8. },"listen-music").start();
  9. while (true){
  10. System.out.println("======浏览网页======");
  11. sleep(1);
  12. }
  13. }
  14. public static void sleep(long mill){
  15. try {
  16. Thread.sleep(mill);
  17. } catch (InterruptedException e) {
  18. e.printStackTrace();
  19. }
  20. }
  21. }

控制台打印

  1. ======浏览网页======
  2. ======听音乐======
  3. ======浏览网页======
  4. ======听音乐======
  5. ======浏览网页======
  6. ======听音乐======
  7. ======听音乐======
  8. .....................................

jconsole 查看线程

01.png

小贴士: 1、main 方法启动时,其本身就是一个线程。 2、Thread 线程,只有调用了 Thread#start 方法,才代表派生出了一个新的线程。

二、线程的生命周期

02.jpg
从图中可知,线程的生命周期大致分为5个主要阶段

1、NEW

new Thread(); new 一个 Thread 对象,此时线程处于 NEW 状态。它只是一个普通的对象,在没有调用 start 方法前,该线程根本不存在。

2、RUNNABLE

调用 Thread#start() 方法。 调用 start 方法后,真正的在 JVM 中创建了一个线程,此时线程并不能马上执行,需要根据 CPU 的调度才能执行,此时属于等待执行的状态,叫做:RUNNABLE 状态【可运行状态】。

RUNNABLE 只能进入 RUNNING 状态,或者意外终止。调用 wait、sleep或其他 block 的 IO 操作,也需要先获取CPU 执行权,也就是先进入 RUNNING 状态。

3、RUNNING

当 CPU 空闲并从任务可执行队列中选中线程,此时线程获取到的 CPU 执行权,此时线程处于 RUNNING 状态。

在 RUNNING 状态时,可能存在的状态变更 1、直接进入 TERMINATED 状态,如:JDK stop 方法,获取存在某个判断结束的标识。 2、进入 BLOCK 状态。如:调用 sleep,或 wait 方法将线程加入 waitSet 中。 3、进入某个阻塞的IO操作。如:因网络数据读写而进入 BLOCKED 状态。 4、获取某个锁,从而加入该锁阻塞的队列中从而进入 BLOCKED 状态。 5、CPU 时间片执行完,CPU 放弃执行该线程,线程进入 RUNNABLE 状态。 6、主动调用 yield 方法,放弃 CPU 执行权,进入 RUNNABLE 状态。

4、BLOCKED

线程因为调用 sleep、wait方法,进入某个阻塞的 IO 操作或者争抢锁资源而进入 BLOCKED 状态。

在 BLOCKED 状态时,可能存在的状态变更 1、直接进入 TERMINATED。如:JDK stop 方法,或者意外死亡。 2、阻塞的 IO 操作结束。如:读取数据结束进入 RUNNABLE 状态。 3、线程完成指定时间的休眠,进入到 RUNNABLE 状态。 4、wait 中的线程被其他线程 notify/notifyAll 唤醒,进入 RUNNABLE 状态 5、线程获取到某个锁资源,进入 RUNNABLE 状态。 6、线程在阻塞过程中被打断,比如其他线程的 interrupt 方法,进入 RUNNABLE 状态。

5、TERMINATED

线程的最终状态,进入该状态,表示线程的生命周期结束。 会进入该状态的操作: 1、线程正常结束,结束生命周期 2、线程运行出错意外结束 3、JVM Crash,导致所有的线程都结束了。

三、Runnable 接口

1、源码分析 Runnable 接口

小贴士:在 JDK 中代表线程的只有 Thread 这个类。

Runnable 是一个简单的接口,只定义了一个无参无返回值的方法,代码如下:

@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

首先、Runnable 是一个函数式接口,可以使用 java8 lambda 表达式。

其次、通过 Runnable#run() 的 java doc 我们可以得知,Runnable#run 会在线程 start 的使用被 Thread#run 调用。

下面我们来看看 Thread#run 方法代码

public class Thread implements Runnable {
  /* What will be run. */
  // 构造函数传入的 Runnable ,如果没有传入则为空
    private Runnable target;

    @Override
    public void run() {
      // 如果存在传入的 Runnable 实现,会执行 Runnable#run 方法
        if (target != null) {
            target.run();
        }
        // 如果 target 为空,则需要重写该方法,写入业务逻辑
    }
}

2、Runnable 接口 对比重写 Thread#run

1、重写 Thread#run 方法是,业务逻辑实在 run 方法中的,换句话说就是,业务逻辑和 Thread 是耦合的。换句话说就是多线程间的 run 方法是不能共享的,也就是线程A不能把线程B的run方法当做自己的执行单元。

2、使用 Runnable 接口,能够将执行单元和 Thread 的操作进行解耦,同时多个线程在构造时能够使用同一套执行单元。

四、多线程一定快吗?

数据验证

对 a、b 两个数进行循环累加操作 并发操作:两个线程分别对 a,b 进行累加操作 串行操作:直接在一个线程内进行 a,b 的累加操作

并发代码

03.jpg

串行代码

04.png

不同操作次数统计表格如下

循环次数 串行执行耗时(ms) 并发执行耗时(ms) 串行并发对比
100万次 5 21 串行快
200万次 6 13 串行快
400万次 7 5

不同操作系统,不同数据量有所不同。

平均来说循环次数较小时,串行比多线程快。

数据量达到一定量时,多线程的平均运行速递比串行快。

结论:线程间存在上下文切换

CPU 单核或是多核都支持多线程执行,这一机制的实现源自于 CPU时间片

CPU时间片 是 CPU 分配给每个线程的执行时间,这个时间是非常短的通常为几十毫秒,一个线程消耗完了换另一个线程来,CPU 通过不断的切换线程执行,让我们感觉多个线程是同时执行的。

一次上下文切换的过程:

  • CPU 通过时间片分配算法循环执行任务
  • 一个任务执行一个时间片后切换到下一个任务,切换前进行任务状态保存

任务从保存再到重新加载执行的过程就是一次上下文切换。