Chapter One:快速认识线程

<h2>线程的介绍</h2>
<p>对计算机来说每一个任务就是一个进程(Process),在每一个进程内部至少要有一个线程(Thread)是在运行中,线程也称为轻量级的进程。</p>
<p>线程是程序执行的一个路径,每一个线程都有自已的局部变量表、程序计数器(指向正在执行的指令指针)以及各自的生命周期。</p>
<h2>快速创建并启动一个线程</h2>
<p>假如想在浏览网页新闻的同时听听音乐,下面 尝试用Java的代码来实现这一功能:</p>
<pre><code class='language-java' lang='java'>import java.util.concurrent.TimeUnit;

public class TryConcurrency {
    public static void main(String[] args) {
        borwseNews();
        enjoyMusic();
    }

    /**
     * Browse the latest news.
     */
    private static void borwseNews() {
        for (;;) {
            System.out.println(&quot;Uh-huh, the good news.&quot;);
            sleep(1);
        }
    }

    /**
     * Listening and enjoy the music
     */
    private static void enjoyMusic() {
        for (;;) {
            System.out.println(&quot;Uh-huh, the nice music.&quot;);
            sleep(1);
        }
    }

    /**
     * Simulate the wait and ignore exception.
     * @param seconds
     */
    private static void sleep(int seconds) {
        try {
            TimeUnit.SECONDS.sleep(seconds);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
</code></pre>
<p>代码试图让听音乐和看新闻两个任务同时执行,不过程序输出永远都是在看新闻,输出如下:</p>
<pre><code class='language-shell' lang='shell'>Uh-huh, the good news.
Uh-huh, the good news.
Uh-huh, the good news.
...
</code></pre>
<h3>并发运行交替输出</h3>
<p>如果想两个事件并发执行,就必须使用Java提供的Thread这个class,如下是修改后的代码:</p>
<pre><code class='language-java' lang='java'>    public static void main(String[] args) {
        // 通过匿名内部类的方式创建线程,并且重写其中的run方法
        new Thread() {
            @Override
            public void run() {
                enjoyMusic();
            }
        }.start();
        borwseNews();
    }
</code></pre>
<p>输出如下:</p>
<pre><code class='language-shell' lang='shell'>Uh-huh, the good news.
Uh-huh, the nice music.
Uh-huh, the good news.
Uh-huh, the nice music.
...
</code></pre>
<p>代码修改之后会发现两个任务可以并行运行,并且在控制台交替输出。</p>
<blockquote><p>以上代码中,线程启动必须在其中一个任务之前,否则线程将永远得不到启动,因为前一个任务永远不会结束。</p>
<ol>
<li>创建一个线程,并且重写其run方法,将enjoyMusic交给它执行。</li>
<li>启动新线程,只有调用了Thread的start方法,才代表派生了一个新的线程,否则Thread和其他普通的Java对象没有什么区别,start方法是一个立即返回方法,并不会让程序陷入阻塞。</li>

</ol>
</blockquote>
<p>如果用Java 8 Lambda改造上面的代码,如下所示:</p>
<pre><code class='language-java' lang='java'>    public static void main(String[] args) {
        new Thread(TryConcurrency::enjoyMusic).start();
        borwseNews();
    }
</code></pre>
<h3>使用Jconsole观察线程</h3>
<p>借助Jconsole或者Jstack命令可以查看JVM中有多少个线程,这两个JVM工具都是由JDK自身提供的。</p>


<p><img src="/home/ykyu/Pictures/Screenshot from 2020-04-12 15-06-55.png" referrerpolicy="no-referrer"></p>
<p>从图中可以清除看到两个线程,其中一个是main,另一个是Thread-0,之前说过在操作系统启动一个Java虚拟机(JVM)的时候,其实是启动了一个进程,而在该进程里面启动了一个以上的线程,其中Thread-0是通过new Thread()对象创建的,main线程是由JVM启动时创建的,J2SE程序的入口就是main函数,当然还有一些其他的守护线程,比如垃圾回收线程、RMI线程等。</p>
<h2>线程的生命周期详解</h2>
<p>夲节来分析一下线程的生命周期,如下图:</p>


<p><img src="Chapter One快速认识线程.assets/Screenshot from 2020-04-12 23-00-08.png" referrerpolicy="no-referrer"></p>
<p>通过图展示可知,线程的生命周期大体可以分为如下5个主要的阶段:</p>
<ul>
<li>NEW</li>
<li>RUNNABLE</li>
<li>RUNNING</li>
<li>BLOCKED</li>
<li>TERMINATED</li>

</ul>
<h3>线程的NEW状态</h3>
<p>当用new创建一个Thread对象时,此时它并不处于执行状态,因为没有调用start方法启动该线程,那么线程的状态为NEW状态,它只是Thread对象的状态,因为在没有start之前,该线程根本不存在,与你用关键字new创建一个普通的Java对象没有什么区别。</p>
<p>NEW状态通过start方法进入RUNNABLE状态。</p>
<h3>线程的RUNNABLE状态</h3>
<p>线程对象进入RUNNABLE状态必须调用start方法,此时才在JVM进程中创建了一个线程,线程一经启动就可以立即得到执行吗?答案是否定的,线程的运行与否和进程一样都要听令与CPU的调度,将这个状态称为可执行状态(RUNNABLE)。</p>
<p>由于存在Running状态,所以不会直接进入BLOCKED状态和TERMINATED状态,即使是在线程的执行逻辑中调用wait\sleep或者其他block的IO操作等,也必须先获得CPU的调度执行权才可以,RUNNABLE的线程只能意外终止或者进入RUNNING状态。</p>
<h3>线程的RUNNING状态</h3>
<p> 一旦CPU通过轮询或者其他方式从任务可执行队列中选中了线程,那么此时它才能真正地执行自已的逻辑代码,一个正在RUNNING状态的线程事实上也是RUNNABLE的,但是反过来则不成立。</p>
<p>在该状态中,线程的状态可以发生如下的状态转换:</p>
<ul>
<li>直接进入TERMINATED状态,比如调用JDK已经不推荐使用的stop方法或者判断某个逻辑标识。</li>
<li>进入BLOCKED状态,比如调用了sleep,或者wait方法而加入了waitSet中。</li>
<li>进入某个阻塞的IO操作,比如因网络数据的读写而进入了BLOCKED状态。</li>
<li>获取某个锁资源,从而加入到该锁的阻塞队列中而进入BLOCKED状态。</li>
<li>线程主动调用yield方法,放弃CPU使用权,进入RUNNABLED状态。</li>

</ul>
<h3>线程的BLOCKED状态</h3>
<p>线程在BLOCKED状态中可以切换至如下几个状态:</p>
<ul>
<li>直接进入TERMINATED状态,比如调用JDK已经不推荐使用的stop方法或者意外死亡(JVM Crash)</li>
<li>线程阻塞的操作结束,比如读取了想要的数据字节进入到RUNNABLED状态。</li>
<li>线程完成了指定时间的休眠,进入到了RUNNABLE状态。</li>
<li>Wait中的线程被其他线程notify/notifyAll唤醒,进入RUNNABLED状态。</li>
<li>线程获取到了某个锁资源,进入RUNNABLED状态。</li>
<li>线程在阻塞过程中被打断,比如其他线程调用了interrupt方法,进入RUNNABLED状态。</li>

</ul>
<h3>线程的TERMINATED状态</h3>
<p>TERMINATED是一个线程的最终状态,在该状态中线程将不会切换到其他任何状态,线程进入TERMINATED状态,意味着该线程的整个生命周期都结束了,下列情况将会使线程计入TERMINATED状态:</p>
<ul>
<li>线程运行正常结束,结束生命周期。</li>
<li>线程运行出错意外结束。</li>
<li>JVM Crash,导致所有的线程都结束。</li>

</ul>
<h2>线程的start方法剖析:模板设计模式在Thread中的应用</h2>
<p>start方法启动了一个线程,并且该线程进入了可执行状态(RUNNABLE),那么重写Thread的run方法,但却调用了start方法,那么run方法和start方法有什么关系了?</p>
<h3>Thread start方法源码分析以及注意事项</h3>
<p>Thread start方法的源码,如下所示:</p>
<pre><code class='language-java' lang='java'>    /**
     * Causes this thread to begin execution; the Java Virtual Machine
     * calls the &lt;code&gt;run&lt;/code&gt; method of this thread.
     * &lt;p&gt;
     * The result is that two threads are running concurrently: the
     * current thread (which returns from the call to the
     * &lt;code&gt;start&lt;/code&gt; method) and the other thread (which executes its
     * &lt;code&gt;run&lt;/code&gt; method).
     * &lt;p&gt;
     * 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 &quot;system&quot;
         * 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 &quot;NEW&quot;.
         */
        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&#39;s list of threads
         * and the group&#39;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 */
            }
        }
    }
</code></pre>
<p>start方法的源码足够简单,其实最核心的部分是start0这个本地方法,也就是JNI方法:</p>
<pre><code class='language-java' lang='java'>    private native void start0();
</code></pre>
<p>也就是说在start方法中会调用start0方法,那么重写的那个run方法何时被调用了?在start方法中如下说明:</p>
<pre><code class='language-java' lang='java'>    /**
     * Causes this thread to begin execution; the Java Virtual Machine
     * calls the &lt;code&gt;run&lt;/code&gt; method of this thread.
     */
</code></pre>
<p>在开始执行这个线程时,JVM将会调用该线程的run方法,换言之,run方法是被JNI方法start0()调用的,通过start的源码将会总结出如下几个知识要点:</p>
<ul>
<li>Thread被构造后的NEW状态,事实上threadStatus这个内部属性为0。</li>
<li>不能两次启动Thread,否则就会出现IllegalThreadStateException异常。</li>
<li>线程启动后将会被加入到一个ThreadGroup中。</li>
<li>一个线程生命周期结束,也就是到了TERMINATED状态,再次调用start方法是不允许的,也就是说TERMINATED状态是没有方法回到RUNNABLE/RUNNING状态的。</li>

</ul>
<h3>模板设计模式在Thread中的应用</h3>
<p>线程的真正的执行逻辑是在run方法中,把run方法称为线程的执行单元。Thread中run方法的代码如下,如果没有使用Runnable接口对其进行构造,则可以认为Thread的run方法本身就是一个空的实现:</p>
<pre><code class='language-java' lang='java'>    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
</code></pre>
<p>其实Thread的run和start就是一个比较典型的模板设计模式,傅磊编写算法结构代码,子类实现逻辑细节。</p>
<h3>Thread模拟营业大厅叫号程序</h3>
<p>用程序模拟一下叫号的过程,约定当天最多受理50笔业务,TicketWindow代表大厅里的出号机器:</p>
<pre><code class='language-java' lang='java'>public class TicketWindow extends Thread {
    private final String name;

    private static final int MAX = 500;
    
    public int index = 1;

    public TicketWindow(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        while (index &lt;= MAX) {
            System.out.println(&quot;柜台: &quot; + name + &quot; 当前的号码是: &quot; + (index++));
        }
    }

    public static void main(String[] args) {
        TicketWindow ticketWindow1 = new TicketWindow(&quot;一号出号机&quot;);
        ticketWindow1.start();

        TicketWindow ticketWindow2 = new TicketWindow(&quot;二号出号机&quot;);
        ticketWindow2.start();
    }
}

</code></pre>
<h3>Runnable接口的引入以及策略模式在Thread中的使用</h3>
<h3>Runnable的职责</h3>
<p>Runnable接口非常简单,只定义了一个无参数无返回值的run方法,具体如下代码所示:</p>
<pre><code class='language-java' lang='java'>public interface Runnable {
    /**
     * When an object implementing interface &lt;code&gt;Runnable&lt;/code&gt; is used
     * to create a thread, starting the thread causes the object&#39;s
     * &lt;code&gt;run&lt;/code&gt; method to be called in that separately executing
     * thread.
     * &lt;p&gt;
     * The general contract of the method &lt;code&gt;run&lt;/code&gt; is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}
</code></pre>
<p>创建线程只有一种方式那就是构造Thread类,而实现线程的执行单元则有两种方式,第一种是重写Thread的run方法,第二种是实现Runnable接口的run方法,并且将Runnable实例用作构造Thread的参数。</p>
<h3>策略模式在Thread中的应用</h3>
<p>无论是Runnable的run方法,还是Thread类本身的run方法(事实上,Thread类也是实现了Runnable接口)都是想将线程的控制本身和业务逻辑的运行分离开来,达到职责分明,功能单一的原则,这一点与GoF设计模式中的策略设计模式很相似。</p>
<blockquote><p>重写Thread类的run方法和实现Runnable接口的run方法还有一个很重要的不同,那就是Thread类的run方法是不能共享的,也就是说A线程不能把B线程的run方法当作自已的执行单元,而使用Runnable接口则很容易就能实现这一点,使用同一个Runnable的实例构造不同的Thread实例。</p>
</blockquote>
<h2>本章总结</h2>
<p>在本章中,学习了什么是线程,以及初步掌握了如何创建一个线程,并且通过重写Thread的run方法和实现Runnable接口的run方法进而实现线程的执行单元。模板设计模式以及策略设计模式和Thread以及Runnable的结合使得大家能够更加清晰的掌握多线程的API是如何实现线程控制和业务执行解耦分离的。</p>
<p>最重要的内容就是线程的生命周期,在使用多线程的过程中,线程的生命周期将会贯穿始终,只有清晰地掌握生命周期各个阶段的切换,才能更好地理解线程的阻塞以及唤醒机制,同时也为掌握同步锁等概念打下基础。</p>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值