Java知识点(四)线程

一、Thread的run()与start()

Java的线程是通过java.lang.Thread类来实现的。VM启动时会有一个由主方法所定义的线程。可以通过创建Thread的实例来创建新的线程。每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,方法run()称为线程体。通过调用Thread类的start()方法来启动一个线程。

二、Thread的五种状态

在Java当中,线程通常都有五种状态:创建、就绪、运行、阻塞和死亡

  • 第一是创建状态:在生成线程对象,并没有调用该对象的start方法,这是线程处于创建状态。
  • 第二是就绪状态:当调用了线程对象的start方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。
  • 第三是运行状态:线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码。
  • 第四是阻塞状态:线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend,wait等方法都可以导致线程阻塞。
    第五是死亡状态:如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡。对于已经死亡的线程,无法再使用start方法令其进入就绪。

三、实现并启动线程有两种方法

  • 写一个类继承自Thread类,重写run方法。用start方法启动线程
  • 写一个类实现Runnable接口,实现run方法。用new Thread(Runnable target).start()方法来启动

查看Thread源码,我们可以发现 Thread 类也实现了Runnable接口

public class Thread implements Runnable {
    ......

    @Override
    public void run() {
        if (target != null) {
            target.run();   // private Runnable target;
        }
    }
   ......
}

四、多线程原理

相当于玩游戏机,只有一个游戏机(cpu),可是有很多人要玩,于是,start是排队,等CPU选中你就是轮到你,你就run(),当CPU的运行的时间片执行完,这个线程就继续排队,等待下一次的run()。

调用start()后,线程会被放到等待队列,等待CPU调度,并不一定要马上开始执行,只是将这个线程置于可动行状态。然后通过JVM,线程Thread会调用run()方法,执行本线程的线程体。先调用start后调用run,这么麻烦,为什么不直接调用run?就是为了实现多线程的优点,没这个start不行。

  • start()方法来启动线程,真正实现了多线程运行。这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码;通过调用Thread类的start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。 然后通过此Thread类调用方法run()来完成其运行操作的, 这里方法run()称为线程体,它包含了要执行的这个线程的内容, run方法运行结束, 此线程终止。然后CPU再调度其它线程。

  • run()方法如果当作普通方法的方式调用,程序还是要顺序执行,要等待run方法体执行完毕后,才可继续执行下面的代码; 程序中只有主线程这一个线程, 其程序执行路径还是只有一条, 这样就没有达到写线程的目的。

多线程就是分时利用CPU,宏观上让所有线程一起执行 ,也叫并发

4.1 调用start()方法是并发执行
public class Client {

    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                int a = 1;
                while(a<=5) {
                    System.out.println("thread excute..........."+a);
                    a++;
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        
        //调用start()方法是并发执行
        t.start();
        
        for(int i = 1;i<=10;i++) {
            System.out.println("i = "+i);
        }   
    }
}

打印结果

i = 1
thread excute...........1
i = 2
i = 3
i = 4
i = 5
i = 6
i = 7
i = 8
i = 9
i = 10
thread excute...........2
thread excute...........3
thread excute...........4
thread excute...........5
4.2 调用run()方法是顺序执行
public class Client {

    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                int a = 1;
                while(a<=5) {
                    System.out.println("thread excute..........."+a);
                    a++;
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        
        //调用run()方法是顺序执行
        t.run();
        
        for(int i = 1;i<=10;i++) {
            System.out.println("i = "+i);
        }   
    }
}

打印结果

thread excute...........1
thread excute...........2
thread excute...........3
thread excute...........4
thread excute...........5
i = 1
i = 2
i = 3
i = 4
i = 5
i = 6
i = 7
i = 8
i = 9
i = 10

五、run()方法是怎么被执行的

5.1 Java线程和OS线程

我们知道,Java的一个线程实际上是对应了操作系统的一个线程。而操作系统实现线程有三种方式:

  • 内核线程实现
  • 用户线程实现
  • 用户线程加轻量级进程混合实现

Java线程在JDK1.2之前,是基于用户线程实现的。而在JDK1.2中,线程模型替换为基于操作系统原生线程模型来实现。而在目前的JDK版本中,操作系统支持怎样的线程模型,在很大程度上决定了Java虚拟机的线程是怎样映射的,这点在不同的平台上没法达成一致。对于Sun JDK来说,它的Windows版本和Linux版本都是使用一对一的线程模型实现的,一条Java线程映射到一条轻量级进程之中。

注:具体参见 «深入理解 JAVA虚拟机»第二版 第12章 Java内存模型与线程(378页)

5.2 Java线程创建

创建方式

public class ThreadTest {
    
    class ThreadExtend extends Thread{
    
        @Override
        public void run() {
            super.run();
            System.out.println("ThreadExtend run......");
        }
    }
    
    
    class RunnableExtend implements Runnable{
        @Override
        public void run() {
            System.out.println("RunnableExtend run......");
        }
    }
    
    
    public void testThread() {
        Thread thread = new ThreadExtend();
        thread.start();
        
        Thread thread2 = new Thread(new RunnableExtend());
        thread2.start();
    }
}

我们看到,无论以哪种方式创建,最终我们都会重写一个叫做 run 的方法,来处理我们的业务逻辑,然而我们都是调用一个start方法,来启动一个线程;那 start方法和run方法之间是一个什么关系呢?从后边的介绍我们将获得这样一个信息:run就是一个回调函数,和我们普通的函数没有区别。

5.3 Java线程的实现

一个 Java 线程的创建本质上就对应了一个本地线程(native thread)的创建,两者是一一对应的。关键问题是:本地线程执行的应该是本地代码,而 Java 线程提供的线程函数(run)是 Java 方法,编译出的是 Java 字节码,所以, Java 线程其实提供了一个统一的线程函数,该线程函数通过 Java 虚拟机调用 Java 线程方法 , 这是通过 Java 本地方法调用来实现的。我们查看Thread源码可以看到:

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

可以看到它实际上调用了本地方法 start0, 而start0声明如下:

   private native void start0();

也就是新创建的线程启动调用native start0方法,而这些native方法的注册是在Thread对象初始化的时候完成的,我们同样来看Thread源码

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

Thread 类有个 registerNatives 本地方法,该方法主要的作用就是注册一些本地方法供 Thread 类使用,如 start0(),stop0() 等等,可以说,所有操作本地线程的本地方法都是由它注册的。这个方法放在一个 static 语句块中,当该类被加载到 JVM 中的时候,它就会被调用,进而注册相应的本地方法。而本地方法 registerNatives 是定义在 Thread.c 文件中的。Thread.c 是个很小的文件,它定义了各个操作系统平台都要用到的关于线程的公用数据和操作,如下:

JNIEXPORT void JNICALL
 2  Java_Java_lang_Thread_registerNatives (JNIEnv *env, jclass cls){ //registerNatives
 3    (*env)->RegisterNatives(env, cls, methods, ARRAY_LENGTH(methods));
 4  }
 5  static JNINativeMethod methods[] = {
 6     {"start0", "()V",(void *)&JVM_StartThread}, //start0 方法
 7     {"stop0", "(" OBJ ")V", (void *)&JVM_StopThread},
 8      {"isAlive","()Z",(void *)&JVM_IsThreadAlive},
 9      {"suspend0","()V",(void *)&JVM_SuspendThread},
10      {"resume0","()V",(void *)&JVM_ResumeThread},
11      {"setPriority0","(I)V",(void *)&JVM_SetThreadPriority},
12      {"yield", "()V",(void *)&JVM_Yield},
13      {"sleep","(J)V",(void *)&JVM_Sleep},
14      {"currentThread","()" THD,(void *)&JVM_CurrentThread},
15      {"countStackFrames","()I",(void *)&JVM_CountStackFrames},
16      {"interrupt0","()V",(void *)&JVM_Interrupt},
17      {"isInterrupted","(Z)Z",(void *)&JVM_IsInterrupted},
18      {"holdsLock","(" OBJ ")Z",(void *)&JVM_HoldsLock},
19      {"getThreads","()[" THD,(void *)&JVM_GetAllThreads},
20      {"dumpThreads","([" THD ")[[" STE, (void *)&JVM_DumpThreads},
21  };

观察上边一小段代码,可以容易的看出 Java 线程调用 start->start0 的方法,实际上会调用到 JVM_StartThread 方法,那这个方法又是怎么处理的呢?
实际上,我们需要看到的是该方法最终要调用 Java 线程的 run 方法,事实的确也是这样的。在 jvm.cpp 中,有如下代码段:

JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread)){
    ...
    native_thread = new JavaThread(&thread_entry, sz);
    ...
}

这里JVM_ENTRY是一个宏,用来定义JVM_StartThread 函数,可以看到函数内创建了真正的平台相关的本地线程,其线程函数是 thread_entry,如下:

static void thread_entry(JavaThread* thread, TRAPS) {
    HandleMark hm(THREAD);
    Handle obj(THREAD, thread->threadObj());
    JavaValue result(T_VOID);
    JavaCalls::call_virtual(&result,obj,
    KlassHandle(THREAD,SystemDictionary::Thread_klass()),
    vmSymbolHandles::run_method_name(),    //看这里! 
    vmSymbolHandles::void_method_signature(),THREAD);
 }

可以看到调用了 vmSymbolHandles::run_method_name 方法,而run_method_name是在 vmSymbols.hpp 用宏定义的:

class vmSymbolHandles: AllStatic {
   ...
    template(run_method_name,"run")  //这里决定了调用的方法名称是 “run”!
   ...
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349