• Java创建线程的细节分析


    转载:http://shmilyaw-hotmail-com.iteye.com/blog/1880902

    前言

        关于线程创建的问题,可以说是老生常谈了。在刚开始学习Thread的时候基本上都会接触到,用简单的一两句话就可以概括起来。一个是创建类实现Runnable接口,然后将该类的实例作为参数传入到Thread构造函数中。再调用Thread对象的start方法。还有一种是继承Thread类,覆写run方法。然后在该对象实例中调用start方法。那么,这两种方式在什么情况下适用呢?还有,既然我们要实现的类都要写run这个方法,为什么在构造的实例里要调用start方法呢?这多麻烦啊,还要绕一个弯,我直接调用run方法不行吗?这样岂不是更省事呢?对于这些问题,我会在本文中进行详细的分析和讨论。

    典型的线程创建过程

        这个过程可以说很简单,主要的两种方式如下:

    一、 实现Runnable接口

    主要三个步骤:

    1. 在类中间实现Runnable接口

    2. 创建该对象实例

    3. 再新建一个Thread对象,将该对象作为参数传入构造函数

    4. 调用Thread对象的start方法。

    见代码如下:

    Java代码  收藏代码
    1. public class ThreadCon implements Runnable  
    2. {  
    3.     public void run()  
    4.     {  
    5.         System.out.println("Entering the test thread...");  
    6.         try  
    7.         {  
    8.             Thread.sleep(5000);  
    9.             System.out.println("Thread executing...");  
    10.             Thread.sleep(5000);  
    11.         }  
    12.         catch(InterruptedException e)  
    13.         {  
    14.             e.printStackTrace();  
    15.         }  
    16.     }  
    17.   
    18.     public static void main(String[] args)  
    19.     {  
    20.         ThreadCon con = new ThreadCon();  
    21.         Thread thread = new Thread(con);  
    22.         thread.start();  
    23.         System.out.println("main thread");  
    24.     }  
    25. }  

     二、继承Thread类

    主要就是两个步骤:

    1. 集成Thread类,实现run方法。

    2. 创建该类的实例,并调用start()方法。

    Java代码  收藏代码
    1. public class ThreadCon extends Thread  
    2. {  
    3.     public void run()  
    4.     {  
    5.         System.out.println("Entering the test thread...");  
    6.         try  
    7.         {  
    8.             Thread.sleep(5000);  
    9.             System.out.println("Thread executing...");  
    10.             Thread.sleep(5000);  
    11.         }  
    12.         catch(InterruptedException e)  
    13.         {  
    14.             e.printStackTrace();  
    15.         }  
    16.     }  
    17.   
    18.     public static void main(String[] args)  
    19.     {  
    20.         ThreadCon thread = new ThreadCon();  
    21.         thread.start();  
    22.         System.out.println("main thread");  
    23.     }  
    24. }  

     看了这两部分代码之后,不禁有几个问题。

    1. 这两种创建线程的方式,在Thread类里面是怎么定义的呢?

    2. 我们继承类或者实现接口,都要覆写run()这个方法,为什么后面要调用start方法呢?他们之间有什么关系呢?

        我们先来看第一个问题。

    线程相关类结构

        如果我们去仔细看Thread类和Runnable接口的实现的话,他们会有这么一个关系:

     Thread类本身就继承了Runnable接口。

    Runnable接口本身的定义很简单:

    Java代码  收藏代码
    1. public interface Runnable {  
    2.     /** 
    3.      * When an object implementing interface <code>Runnable</code> is used 
    4.      * to create a thread, starting the thread causes the object's 
    5.      * <code>run</code> method to be called in that separately executing 
    6.      * thread. 
    7.      * <p> 
    8.      * The general contract of the method <code>run</code> is that it may 
    9.      * take any action whatsoever. 
    10.      * 
    11.      * @see     java.lang.Thread#run() 
    12.      */  
    13.     public abstract void run();  
    14. }  

     Thread类中间定义的run方法实现了Runnable接口的规范:

    Java代码  收藏代码
    1. @Override  
    2.     public void run() {  
    3.         if (target != null) {  
    4.             target.run();  
    5.         }  
    6.     }  

      上面这段代码里的target是一个Runnable的成员变量,在Thread里面的定义如下:

    Java代码  收藏代码
    1. /* What will be run. */  
    2. private Runnable target;  

        另外,再看到Thread类中间有一个如下定义的构造函数:

    Java代码  收藏代码
    1. public Thread(Runnable target) {  
    2.         init(null, target, "Thread-" + nextThreadNum(), 0);   
    3. }  

         我们就不难理解,我们通过构造一个实现Runnable接口的对象,将它作为参数传入Thread构造函数的原因了。这正好对应了我们第一种创建以及启动线程的过程。

        对于第二种方式来说,当我们将一个类继承Thread的时候,通过覆写run方法,我们已经继承了Thread的定义,所以只要调用start方法就对应了我们第二种创建以及启动线程的过程。

        写到这里的时候,我们会发现,还有一个遗漏了的地方。刚才只是讨论了我们要创建的对象和Runnable, Thread之间的关系。但是start方法到底怎么样,他们到底有什么关系呢?我们接下来很快就明白了。

    start方法的秘密

        我们来看看Thread里面start()方法的实现:

    Java代码  收藏代码
    1. /** 
    2.      * Causes this thread to begin execution; the Java Virtual Machine 
    3.      * calls the <code>run</code> method of this thread. 
    4.      * <p> 
    5.      * The result is that two threads are running concurrently: the 
    6.      * current thread (which returns from the call to the 
    7.      * <code>start</code> method) and the other thread (which executes its 
    8.      * <code>run</code> method). 
    9.      * <p> 
    10.      * It is never legal to start a thread more than once. 
    11.      * In particular, a thread may not be restarted once it has completed 
    12.      * execution. 
    13.      * 
    14.      * @exception  IllegalThreadStateException  if the thread was already 
    15.      *               started. 
    16.      * @see        #run() 
    17.      * @see        #stop() 
    18.      */  
    19.     public synchronized void start() {  
    20.         /** 
    21.          * This method is not invoked for the main method thread or "system" 
    22.          * group threads created/set up by the VM. Any new functionality added 
    23.          * to this method in the future may have to also be added to the VM. 
    24.          * 
    25.          * A zero status value corresponds to state "NEW". 
    26.          */  
    27.         if (threadStatus != 0)  
    28.             throw new IllegalThreadStateException();  
    29.   
    30.         /* Notify the group that this thread is about to be started 
    31.          * so that it can be added to the group's list of threads 
    32.          * and the group's unstarted count can be decremented. */  
    33.         group.add(this);  
    34.   
    35.         boolean started = false;  
    36.         try {  
    37.             start0();  
    38.             started = true;  
    39.         } finally {  
    40.             try {  
    41.                 if (!started) {  
    42.                     group.threadStartFailed(this);  
    43.                 }  
    44.             } catch (Throwable ignore) {  
    45.                 /* do nothing. If start0 threw a Throwable then 
    46.                   it will be passed up the call stack */  
    47.             }  
    48.         }  
    49.     }  
    50.   
    51.     private native void start0();  

       这部分的代码实际上并不长,这里主要的目的是调用了start0()这个方法。而start0这个方法在代码里的定义是一个private native的方法。这意味着什么呢?表示实际上这个start0方法是一个本地实现的方法。这个本地方法采用的不一定是java实现的,也可能是一个其他语言的实现,用来在JVM里面创建一个线程空间,再把run方法里面定义的指令加载到这个空间里。这样才能实现新建立一个线程的效果。所以,这也就是start方法能够创建线程的魔法之所在。

        同时,这里也解释了为什么我们不能直接去调用run方法来期望它生成一个新的线程。如果我们直接调用run方法的话,它实际上就是在我们的同一个线程里面顺序执行的。在前面启动新线程的情况下,我们的代码执行结果会像如下的情况:

    Java代码  收藏代码
    1. main thread  
    2. Entering the test thread...  
    3. executing...  

     如果我们尝试将main函数里面的thread.start()改成thread.run(),我们会发现执行的结果如下:

    Java代码  收藏代码
    1. Entering the test thread...  
    2. Thread executing...  
    3. main thread  

         这种情况下run()方法和main函数其实是在同一个线程空间里执行。如果我们用一些profile工具进行分析的话,也会看到这样的场景。

     两种创建线程方法的比较

        通过继承的方式实现线程的方式用起来比较简单直接。但是从设计的角度来说,这个定义的类就需要和Thread方法有继承的关联关系。如果这个类本身又有其他的继承关系要处理呢?这个时候,这种方式就不是那么合适了。在Java本身的设计思路以及我们在面向对象设计思想的指导里,都不推荐优先考虑继承的关系。一个类可以实现多个接口,但是只能继承一个父类。所以说,采用实现Runnable接口的方式会是更加推荐的做法。

    总结

        对于线程的创建方式来说,实现接口的方法相对更加具有灵活性。因为这种方式不会和一个类建立固定的继承关系,可以更好的后续扩展一些。创建线程要调用start方法是因为start方法里面的一个特殊效果,它才能导致run方法的指令在新建的线程里执行。如果我们在建立实现runnable接口或者继承Thread对象的实例里直接调用run方法,这是不会产生新线程的。这也是为什么我们一定要调用start方法的原因。

  • 相关阅读:
    ​Docker 数据卷的管理及自动构建docker镜像
    写代码有这16个好习惯,可以减少80%非业务的bug
    启动Docker“Got permission denied while trying to connect to the Docker daemon socket“问题(亲测可用)
    Docker从入门到干活,看这一篇足矣 [建议收藏]
    docker技术入门与精通(2020.12笔记总结)
    MySQL相关 死锁的发生和避免
    使用docker运行zabbixserver
    Cloudflare 是谁?
    扛得住的MySQL数据库架构
    好未来第一届PHP开源技术大会资料分享
  • 原文地址:https://www.cnblogs.com/googlemeoften/p/5769160.html
Copyright © 2020-2023  润新知