高并发编程从入门到精通(二)

2,083 阅读15分钟

面试中最常被虐的地方一定有并发编程这块知识点,无论你是刚刚入门的大四萌新还是2-3年经验的CRUD怪,也就是说这类问题你最起码会被问3年,何不花时间死磕到底。消除恐惧最好的办法就是面对他,奥利给!(这一系列是本人学习过程中的笔记和总结,并提供调试代码供大家玩耍)

上章回顾

1.并发编程主要解决的问题是什么?

2.线程创建和实现的方式有哪些?

3.new Thread()是如何一穿三的呢?

请自行回顾以上问题,如果还有疑问的自行回顾上一章哦~

本章提要

本章学习完成,你将会对线程的生命周期有清楚的认识,并且明白不同状态之间是如何转换的,以及对java线程状态枚举类解读。此外本章还会着重对Thread.start()进行细致的解读和分析,同时通过案例你可以更加清晰的明白start()run()之间的关系。(老规矩,熟悉这块的同学可以选择直接点赞👍完成本章学习哦)

本章代码下载

一、线程生命周期

相信有不少同学看到这四个字就气不打一出来,面试总是被问及这生命周期是啥,那生命周期你来描述一下的,一下脾气就上来了。确实这个东西是有点烦,但是个人没什么技巧,死记硬背是最简单最好用的方式。等到背熟了,自然而然在用到的时候就会融会贯通,甚至会衍生出自己的独有的思路。不说太多,概念性的东西背就完事儿了,上菜~

1.NEW阶段

NEW阶段就是你new Thread()创建线程对象时候的阶段。

划重点了

NEW阶段下其实线程根本还是不存在的,我们只是创建了一个Therad对象,就和我们最常用的new关键字是一个道理。只有当我们真正把线程启动起来的时候,此时才会在JVM进程中把我们的线程创建出来。

按照上一章的思路,我们new了一个Thread对象之后就需要调用Thread.start()来启动线程,此时线程会从NEW阶段转换到RUNNABLE阶段。

2. RUNNABLE阶段

只有调用Thread.start()方法才能使线程从NEW阶段转换到RUNNABLE阶段。

当然我们从字面意思也可以知道此时线程是处于可执行转状态而不是真正的执行中状态了,此时的线程只能等CPU翻牌子,翻到了他才能真正的跑起来。有些同学可能会说要是CPU一直不翻牌子咋办?严格意义上来讲,处在RUNNABLE的线程只有两条出路,一条是线程意外退出,还有一条是被CPU翻牌子进入RUNNING阶段。


到这里一切看起来还是那么简单,那么美好,new一个Thread对象,然后调用start启动起来,然后等翻CPU翻牌子之后进入RUNNING阶段。可以打个比方,NEW阶段的时候我们的线程还是宫外的一位佳人对象,调用start方法之后就摇身一变成为宫里的一位小主了,也就是中间阶段RUNNABLE,等到获取到CPU调度执行权的时候就晋升为得宠的娘娘了,也就是进入了RUNNING阶段。可想而知,此时我们的线程娘娘一定是比宫外的佳人要复杂的多了。


3.RUNNING阶段

⚠️注意

有了解过这块内容的同学看到这里可能会有疑问,java线程状态中并没有这个状态,为什么我们在讲生命周期的时候会把这一状态单独拆分出来做讲解?为了章节内容的流畅性,这块内容的解释放到下一节去讲解,这边我们还是继续讲我们的线程生命周期


好的我们继续

这个阶段的线程已经获取到了CPU调度执行权,也就是说处于运行中状态了。

在该阶段中,线程可以向前或者向后发生转换:

1.由于CPU的调度器轮询导致该线程放弃执行,就会进入RUNNABLE阶段。

2.线程主动调用yield,放弃CPU执行权,就会进入RUNNABLE阶段(这种方式并不是百分百生效的,在CPU资源不紧张的时候不会生效)。

3.调用sleepwait方法,进入BLOCKED阶段(这里讲的BLOCKED阶段和线程的BLOCKED状态需要区分开,这边讲的是一个比较广义的BLOCKED的阶段

4.进行某个阻塞的IO操作而进入BLOCKED阶段

5.为了获取某个锁资源而加入到该锁到阻塞队列中而进入BLOCKED阶段

6.线程执行完成或者调用stop方法或者判断某个逻辑标识,直接进入TERMINATED阶段

4.BLOCKED阶段

进入该阶段的原因已经在RUNNING阶段阐述过了,这里就不再说明,这里主要介绍一下处于该阶段的线程可以如何切换。 1.直接进入TERMINATED,比如调用stop方法或者意外死亡(JVM Crash)

2.线程阻塞的操作结束,读取或者写入了想操作的数据进入RUNNABLE状态

3.线程完成了指定时间的休眠,进入RUNNABLE状态

4.Wait状态的线程被notify或者notifyall唤醒,进入RUNNABLE状态

5.获取到了锁资源,进入RUNNABLE状态

6.线程阻塞过程被打断,比如调用interrupt方法,进入RUNNABLE状态

5.TERMINATED状态

TERMINATED状态是一个线程的最终状态,在该状态中线程不会切换到其他任何状态,线程进入TERMINATED状态意味着线程整个生命周期结束了,进入TERMINATED状态的方式有以下三种:

1.线程正常结束

2.线程意外结束

3.JVM Crash

二、线程生命周期和java线程状态如何对应起来(源码分析)

面对这个问题,最直接的论证方式一定就是看代码,请同学们先找到java.lang.Thread.State这个枚举类。

public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}

可以看到这边state枚举类包含有6中线程状态,根据说明我们一一来解读这六个状态。

(1)NEW状态 = NEW阶段

     * <li>{@link #NEW}<br>
     *     A thread that has not yet started is in this state.
     *     </li>

源码清楚地说明了NEW状态就是一个线程刚刚被创建,但是还没有启动地时候所处的状态,这个和我们上一小节中地NEW阶段能够对应起来这里就不多说了。

(2)RUNNABLE状态 = RUNNABLE阶段+RUNNING阶段

 * <li>{@link #RUNNABLE}<br>
     *     A thread executing in the Java virtual machine is in this state.
     *     </li>

这段的说明意思是在java虚拟机中执行的线程所处的状态称之为RUNNABLE。 也就是说我们上一节中分开讲解的RUNNABLE阶段RUNNING阶段在线程状态中来看统一都称之为RUNNABLE状态,之所以我们在生命周期划分的时候把这两个状态拆分开来看是因为这两个状态差别还是很大的,RUNNING状态的线程比RUNNABLE状态的线程更加复杂一些。

(3)BLOCKED状态+WAITING状态+TIMED_WAITING状态 = BLOCKED阶段

 * <li>{@link #BLOCKED}<br>
     *     A thread that is blocked waiting for a monitor lock
     *     is in this state.
     *     </li>
     * <li>{@link #WAITING}<br>
     *     A thread that is waiting indefinitely for another thread to
     *     perform a particular action is in this state.
     *     </li>
     * <li>{@link #TIMED_WAITING}<br>
     *     A thread that is waiting for another thread to perform an action
     *     for up to a specified waiting time is in this state.
     *     </li>

源码中对于这三个状态对叙述是这样的

BLOCKED:等待获取监视器锁而进入阻塞队列的线程所处的状态

WAITING:无限期等待另一线程执行特定操作的线程处于此状态。

TIMED_WAITING:等待另一线程执行操作的线程在指定的等待时间内处于此状态。

同学们对于BLOCKEDWAITING这两个状态的区别清楚吗?清楚的直接进入(4)哦~

我们还是通过源码中对于state枚举值的描述来进入主题。 state中对于BLOCKED状态的描述如下:

 /**
         * Thread state for a thread blocked waiting for a monitor lock.
         * A thread in the blocked state is waiting for a monitor lock
         * to enter a synchronized block/method or
         * reenter a synchronized block/method after calling
         * {@link Object#wait() Object.wait}.
         */
        BLOCKED

源码中对于BLOCKED这一状态的叙述是这样的,线程在等待获取一个monitor lock的时候该线程就是处于BLOCKED。说通俗一点就是被synchronized关键字描述的同步块或者方法,此时其他线程想要获取这个被描述的同步块或者方法的时候,这个线程就会进去等待获取monitor lock的时候,也就是进入来BLOCKED状态。这里的关键是monitor lock监视锁。

唤醒方式是目标监视锁monitor lock主动释放,这时候我们去竞争这个监视锁并成功之后。

state中对于WAITING状态的描述如下:

/**
         * Thread state for a waiting thread.
         * A thread is in the waiting state due to calling one of the
         * following methods:
         * <ul>
         *   <li>{@link Object#wait() Object.wait} with no timeout</li>
         *   <li>{@link #join() Thread.join} with no timeout</li>
         *   <li>{@link LockSupport#park() LockSupport.park}</li>
         * </ul>
         *
         * <p>A thread in the waiting state is waiting for another thread to
         * perform a particular action.
         *
         * For example, a thread that has called <tt>Object.wait()</tt>
         * on an object is waiting for another thread to call
         * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
         * that object. A thread that has called <tt>Thread.join()</tt>
         * is waiting for a specified thread to terminate.
         */
        WAITING

这里给我们描述了哪些情况下我们的线程会进入WAITING状态,分别是调用Object.waitThread.joinLockSupport.park三个API接口才会进入WAITING状态,这边对于线程API不做具体阐述,后面会单独开一章来介绍,我们这边就先不纠结。

也说明了WAITING状态是当一个线程处于等待另一个线程完成一个特定操作时候所处的状态,这里的关键是两个线程,目标线程等待另一个线程完成某个动作

唤醒方式是等待其他线程完成自己逻辑之后,调用notify或者notiffyall唤醒处于WAITING状态的线程。

(4)TERMINATED状态 = TERMINATED状态


到这里同学们应该已经清楚了线程生命周期以及各个阶段之间的转换,同时明白了不同阶段的生命周期所对应的java线程状态。接下来是本章第二段源码分析啦~

划重点啦,看累的同学休息休息,接下来是start()源码分析

三、Thread.start()设计模式源码解读

开始之前我们先思考一下,为什么start()能启动我们的线程,run()不行呢?

废话不说,先上源码

/**
     * Causes this thread to begin execution; the Java Virtual Machine
     * calls the <code>run</code> method of this thread.
     * <p>
     * The result is that two threads are running concurrently: the
     * current thread (which returns from the call to the
     * <code>start</code> method) and the other thread (which executes its
     * <code>run</code> method).
     * <p>
     * It is never legal to start a thread more than once.
     * In particular, a thread may not be restarted once it has completed
     * execution.
     *
     * @exception  IllegalThreadStateException  if the thread was already
     *               started.
     * @see        #run()
     * @see        #stop()
     */
public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        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 */
            }
        }
    }

老套路,我们先看方法说明

/**
     * Causes this thread to begin execution; the Java Virtual Machine
     * calls the <code>run</code> method of this thread.
     * <p>
     * The result is that two threads are running concurrently: the
     * current thread (which returns from the call to the
     * <code>start</code> method) and the other thread (which executes its
     * <code>run</code> method).
     * <p>
     * It is never legal to start a thread more than once.
     * In particular, a thread may not be restarted once it has completed
     * execution.
     *
     * @exception  IllegalThreadStateException  if the thread was already
     *               started.
     * @see        #run()
     * @see        #stop()
     */

翻译start()方法使线程开始运行,java虚拟机会调用run()方法来执行线程的逻辑单元。并且线程只允许启动一次,多次启动线程是不合法的,会抛出IllegalThreadStateException异常。

下面我们通过三步来解读Thread.start()

(1) 判断threadStatus值

 /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

threadStatus==0 表示NEW状态,如果在调用Thread.start()到时候发现threadStatus!=0那么表示线程已经不处于NEW状态了,此时调用该方法是不合法的,所以抛出IllegalThreadStateException异常。(这么简单的逻辑判断,相信同学们一定和我一样生出了一个想法“我上我也行😁”)

(2) 加入线程组

       /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

翻译:通知组新线程已经被启动,需要添加到线程组中,同时减少未开始计数。 看完翻译,不知所云,我们继续追踪add

void add(Thread t) {
        synchronized (this) {
            if (destroyed) {
                throw new IllegalThreadStateException();
            }
            if (threads == null) {
                threads = new Thread[4];
            } else if (nthreads == threads.length) {
                threads = Arrays.copyOf(threads, nthreads * 2);
            }
            threads[nthreads] = t;

            // This is done last so it doesn't matter in case the
            // thread is killed
            nthreads++;

            // The thread is now a fully fledged member of the group, even
            // though it may, or may not, have been started yet. It will prevent
            // the group from being destroyed so the unstarted Threads count is
            // decremented.
            nUnstartedThreads--;
        }
    }

add方法我们也分为三小步来看

第一步: 判断线程组是否已经被销毁了,如果线程组已经不存在了,那就抛出IllegalThreadStateException异常。

 if (destroyed) {
                throw new IllegalThreadStateException();
            }

第二步:把线程加入到线程组数组中。

if (threads == null) {
                threads = new Thread[4];
            } else if (nthreads == threads.length) {
                threads = Arrays.copyOf(threads, nthreads * 2);
            }
            threads[nthreads] = t;

            // This is done last so it doesn't matter in case the
            // thread is killed
            nthreads++;

首先判断threads是否为空,为空则创建一个数组,初始化长度为4。如果threads不为空,判断nthreads == threads.length如果为true,则对threads数组进行扩容threads = Arrays.copyOf(threads, nthreads * 2),把入参添加到threads[]中,对下标进行累加nthreads++

第三步:未开始线程计数减一

// The thread is now a fully fledged member of the group, even
            // though it may, or may not, have been started yet. It will prevent
            // the group from being destroyed so the unstarted Threads count is
            // decremented.
            nUnstartedThreads--;

(3) 调用start0()方法启动线程

 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 */
            }
        }
        

started是线程的启动状态,预设为false,启动成功再赋值为true。然后调用start0()方法

    private native void start0();

至此我们回想一下我们的线程执行逻辑单元是写在run()中的,那么全程发现没有地方在调用run(),只有这个start0()方法最为可疑。可以看到这个方法是一个本地方法,在Thread.java类定义的开头有静态块来完成注册。

 /* Make sure registerNatives is the first thing <clinit> does. */
    private static native void registerNatives();
    static {
        registerNatives();
    }

好的到此打住,之后会涉及到JVM底层的一些内容,这里我们就不做扩展,我们只要知道start()内部调用调用start0()方法,最终是在创建完我们的线程之后,在线程内部调用来run()方法。

我们只要记清楚一点,真正创建线程和调用run方法的地方是由jvm来完成的,我们这里只是作为一个启动发起方完成一个发送线程启动信号的动作就行。我们这里不再带大家去一堆CPP文件里面去翻云覆雨了,以免晕车。

到这里我们应该清楚了本节开始时候的问题了,为什么start()能启动我们的线程,run()不行呢。相信大家已经知道了原因了,因为run只能作为是一个内部实现类,如果单纯调用run方法的话,我们只是在本线程中实现了逻辑而并没有真正地开辟出一个新的线程来执行我们的逻辑。开辟新的线程需要调用start0()来让JVM帮我们完成创建,所以只有调用start()方法才能启动线程。

这里我也写了一个小例子来验证:

 public static class TestStartAndRun implements Runnable{

    @Override
    public void run() {

      System.out.println("我的名字叫"+Thread.currentThread().getName());
    }
  }


  public static void main(String[] args) {

    new Thread(new TestStartAndRun(),"start").start();

    new Thread(new TestStartAndRun(),"run").run();

  }

输出:

我的名字叫start
我的名字叫main

可以看到,我们定义的名称叫做run的线程并没有启动起来,执行Thread.run()的是我们的main线程,这就证明来run不能创建线程,但是能在当前线程中执行内部类run的逻辑单元,并且可以执行多次。

四、扩展阅读——模仿start的模版设计模式自己写一个类似的程序

/**
   * 杯子
   */
  final void Teacup(String nothing){
    System.out.print("今天");
    Brewing(nothing);
    System.out.println(",愉快的一天开始啦!");
  }

  /**
   * 泡点什么呢
   * @param nothing
   */
  void Brewing(String nothing){
  }

  public static void main(String[] args) {
    TemplateDesignExample t1 = new TemplateDesignExample(){
      @Override
      void Brewing(String nothing){
        System.out.print("下雨,"+nothing);
      }
    };
    t1.Teacup("早上喝咖啡");


    TemplateDesignExample t2 = new TemplateDesignExample(){
      @Override
      void Brewing(String nothing){
        System.out.print("晴天,"+nothing);
      }
    };
    t2.Teacup("早上喝茶");
  }

输出:

今天下雨,早上喝咖啡,愉快的一天开始啦!
今天晴天,早上喝茶,愉快的一天开始啦!

这样不管是下雨还是晴天,我们都可以喝到自己想喝的,每一天都可以愉快地编码,如果能点点关注,点点赞👍,每一天都是双倍的快乐哦!

祝同学们端午小长假快乐呀,放假也不要忘记和我一起学习哦~