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

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

上章回顾

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

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

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

请自行回顾以上问题,若是还有疑问的自行回顾上一章哦~编程

本章提要

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

本章代码下载数组

1、线程生命周期

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

1.NEW阶段

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

划重点了jvm

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

2、线程生命周期和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()源码分析

3、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的逻辑单元,而且能够执行屡次。

4、扩展阅读——模仿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("早上喝茶");
  }
复制代码

输出:

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

这样不论是下雨仍是晴天,咱们均可以喝到本身想喝的,每一天均可以愉快地编码,若是能点点关注,点点赞👍,每一天都是双倍的快乐哦!

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

相关文章
相关标签/搜索