`

线程的发展历史

阅读更多

了解进程、线程模型

       

每次学习一个新技术,我会先去了解这个技术的背景,这个过程看似浪费时间,其实在后续的学习过程中,能够促进理解很多问题。所以对于线程这个概念,我会先从操作系统讲起。因为操作系统的发展带来了软件层面的变革。 从多线程的发展来看,可以操作系统的发展分为三个历史阶段:

  • 真空管和穿孔卡片

  • 晶体管和批处理系统

  • 集成电路和多道程序设计

最早的计算机只能解决简单的数学运算问题,比如正弦、余弦等。运行方式:程序员首先把程序写到纸上,然后穿孔成卡票,再把卡片盒带入到专门的输入室。输入室会有专门的操作员将卡片的程序输入到计算机上。计算机运行完当前的任务以后,把计算结果从打印机上进行输出,操作员再把打印出来的结果送入到输出室,程序员就可以从输出室取到结果。然后,操作员再继续从已经送入到输入室的卡片盒中读入另一个任务重复上述的步骤。

操作员在机房里面来回调度资源,造成计算机存在大量的空闲状态 。而当时的计算机是非常昂贵的,人们为了减少这种资源的浪费。就采用了 批处理系统来解决

批处理操作系统的运行方式:在输入室收集全部的作业,然后用一台比较便宜的计算机把它们读取到磁带上。然后把磁带输入到计算机,计算机通过读取磁带的指令来进行运算,最后把结果输出磁带上。批处理操作系统的好处在于,计算机会一直处于运算状态,合理的利用了计算机资源。(运行流程如下图所示)

(注:此图来源于现代操作系统)

 

批处理操作系统虽然能够解决计算机的空闲问题,但是当某一个作业因为等待磁盘或者其他I/O操作而暂停,那CPU就只能阻塞直到该I/O完成,对于CPU操作密集型的程序,I/O操作相对较少,因此浪费的时间也很少。但是对于I/O操作较多的场景来说,CPU的资源是属于严重浪费的。

多道程序设计的出现解决了这个问题,就是把内存分为几个部分,每一个部分放不同的程序。当一个程序需要等待I/O操作完成时。那么CPU可以切换执行内存中的另外一个程序。如果内存中可以同时存放足够多的程序,那CPU的利用率可以接近100%。 在这个时候,引入了第一个概念- 进程, 进程的本质是一个正在执行的程序,程序运行时系统会创建一个进程,并且给每个进程分配独立的内存地址空间保证每个进程地址不会相互干扰。同时,在CPU对进程做时间片的切换时,保证进程切换过程中仍然要从进程切换之前运行的位置出开始执行。所以进程通常还会包括程序计数器、堆栈指针。

 

有了进程以后,可以让操作系统从宏观层面实现多应用并发。而并发的实现是通过CPU时间片不端切换执行的。对于单核CPU来说,在任意一个时刻只会有一个进程在被CPU调度

 

有了进程以后,为什么还会出现线程呢?

在一个应用进程中,会存在多个同时执行的任务,如果其中一个任务被阻塞,将会引起不依赖该任务的任务也被阻塞。举个具体的例子来说,我们平常用word文档编辑内容的时候,都会有一个自动保存的功能,这个功能的作用是,当计算机出现故障的情况下如果用户未保存文档,则能够恢复到上一次自动保存的点。假设word的自动保存因为磁盘问题导致写入较慢,势必会影响到用户的文档编辑功能,直到磁盘写入完成用户才可编辑,这种体验是很差的。如果我们把一个进程中的多个任务通过线程的方式进行隔离,那么按照前面提到的进程演进的理论来说,在单核心CPU架构中可以通过CPU的时间片切换实现线程的调度充分利用CPU资源以达到最大的性能。

 

我们用了比较长的篇幅介绍了进程、线程发展的历史。总的来说是人们对于计算机的要求越来越高;对于计算机本身的资源的利用率也在不断提高。

02

线程的优势

前面分析了线程的发展历史,这里简单总结一下线程有的优势如下

  • 线程可以认为是轻量级的进程,所以线程的创建、销毁要比进程更快

  • 从性能上考虑,如果进程中存在大量的I/O处理,通过多线程能够加快应用程序的执行速度(通过CPU时间片的快速切换)。

  • 由于线程是CPU的最小调度单元,所以在多CPU架构中能够实现真正的并行执行。每一个CPU可以调度一个线程

这里有两个概念很多人没有搞明白,就是并行和并发

并行:同时执行多个任务,在多核心CPU架构中,一个CPU核心运行一个线程,那么4核心CPU,可以同时执行4个线程

并发:同处理多个任务的能力,通常我们会通过TPS或者QPS来表示某某系统支持的并发数是多少。

总的来说,并行是并发的子集。也就是说我们可以写一个拥有多线程并行的程序,如果在没有多核心CPU来执行这些线程,那就不能以并行的方式来运行程序中的多个线程。所以并发程序可以是并行的,也可以不是。Erlang之父Joe Armstrong通过一张图型的方式来解释并发和并行的区别,图片如下

03

线程的生命周期

线程是存在生命周期的,从线程的创建到销毁,可能会经历6种不同的状态,但是在一个时刻线程只能处于其中一种状态

  • NEW:初始状态,线程被创建时候的状态,还没有调用start方法

  • RUNNABLE:运行状态,运行状态包含就绪和运行两种状态,因为线程启动以后,并不是立即执行,而是需要通过调度去分配CPU时间片

  • BLOCKED:阻塞状态,当线程去访问一个加锁的方法时,如果已经有其他线程获得锁,那么当前线程会处于阻塞状态

  • WAITING:等待状态,设置线程进入等待状态等待其他线程做一些特定的动作进行触发

  • TIME_WAITING:超时等待状态,和WAITING状态的区别在于超时以后自动返回

  • TERMINATED:终止状态,线程执行完毕

下图整理了线程的状态变更过程及变更的操作,每一个具体的操作原理,我会在后续的文章中进行详细分析。

这里有一个问题大家可能搞不明白,BLOCKED和WAITING这两个阻塞有什么区别?

  • BLOCKED状态是指当前线程在等待一个获取锁的操作时的状态。

  • WAITING是通过Object.wait或者Thread.join、LockSupport.park等操作实现的

  • BLOCKED是被动的标记,而WAITING是主动操作

  • 如果说得再深入一点,处于WAITING状态的线程,被唤醒以后,需要进入同步队列去竞争锁操作,而在同步队列中,如果已经有其他线程持有锁,则线程会处于BLOCKED状态。所以可以说BLOCKED状态是处于WAITING状态的线程重新唤醒的必经的状态

 

04

线程的应用场景

线程的出现,在多核心CPU架构下实现了真正意义上的并行执行。也就是说,一个进程内多个任务可以通过多线程并行执行来提高程序运行的性能。那线程的使用场景有哪些呢?

  • 执行后台任务,在很多场景中,可能会有一些定时的批量任务,比如定时发送短信、定时生成批量文件。在这些场景中可以通过多线程的来执行

  • 异步处理,比如在用户注册成功以后给用户发送优惠券或者短信,可以通过异步的方式来执行,一方面提升主程序的执行性能;另一方面可以解耦核心功能,防止非核心功能对核心功能造成影响

  • 分布式处理,比如fork/join,将一个任务拆分成多个子任务分别执行

  • BIO模型中的线程任务分发,也是一种比较常见的使用场景,一个请求对应一个线程

 

 

合理的利用多线程,可以提升程序的吞吐量。同时,还可以通过增加CPU的核心数来提升程序的性能,这就体现了伸缩性的特点

 

 

并发编程的挑战

引入多线程的目的在第一篇提到过,就是为了充分利用CPU是的程序运行得更快,当然并不是说启动的线程越多越好。在实际使用多线程的时候,会面临非常多的挑战

线程安全问题

线程安全问题值的是当多个线程访问同一个对象时,如果不考虑这些运行时环境采用的调度方式或者这些线程将如何交替执行,并且在代码中不需要任何同步操作的情况下,这个类都能够表现出正确的行为,那么这个类就是线程安全的 比如下面的代码是一个单例模式,在代码的注释出,如果多个线程并发访问,则会出现多个实例。导致无法实现单例的效果

  1. publicclassSingletonDemo{

  2.    privatestaticSingletonDemo singletonDemo=null;

  3.    privateSingletonDemo(){}

  4.    publicstaticSingletonDemo getInstance(){

  5.        if(singletonDemo==null){/***线程安全问题***/

  6.            singletonDemo=newSingletonDemo();

  7.        }

  8.        return singletonDemo;

  9.    }

  10. }

通常来说,我们把多线程编程中的线程安全问题归类成如下三个,至于每一个问题的本质,在后续的文章中我们会单独讲解

  • 原子性

  • 可见性

  • 有序性

上下文切换问题

在单核心CPU架构中,对于多线程的运行是基于CPU时间片切换来实现的伪并行。由于时间片非常短导致用户以为是多个线程并行执行。而一次上下文切换,实际就是当前线程执行一个时间片之后切换到另外一个线程,并且保存当前线程执行的状态这个过程。上下文切换会影响到线程的执行速度,对于系统来说意味着会消耗大量的CPU时间。

减少上下文切换的方式

  • 无锁并发编程,在多线程竞争锁时,会导致大量的上下文切换。避免使用锁去解决并发问题可以减少上下文切换

  • CAS算法,CAS是一种乐观锁机制,不需要加锁

  • 使用与硬件资源匹配合适的线程数

死锁

在解决线程安全问题的场景中,我们会比较多的考虑使用锁,因为它使用比较简单。但是锁的使用如果不恰当,则会引发死锁的可能性,一旦产生死锁,就会造成比较严重的问题:产生死锁的线程会一直占用锁资源,导致其他尝试获取锁的线程也发生死锁,造成系统崩溃以下是死锁的简单案例

  1. publicclassDeadLockDemo{

  2.    //定义锁对象

  3.    privatefinalObject lockA =newObject();

  4.    privatefinalObject lockB =newObject();

  5.    privatevoid deadLock(){

  6.        newThread(()->{

  7.            synchronized(lockA){

  8.                try{

  9.                    Thread.sleep(4000);

  10.                }catch(InterruptedException e){

  11.                    e.printStackTrace();

  12.                }

  13.                synchronized(lockB){

  14.                    System.out.println("Lock B");

  15.                }

  16.            }

  17.        }).start();

  18.        newThread(()->{

  19.            synchronized(lockB){

  20.                synchronized(lockA){

  21.                    System.out.println("Lock A");

  22.                }

  23.            }

  24.        }).start();

  25.    }

  26.    publicstaticvoid main(String[] args){

  27.        newDeadLockDemo().deadLock();

  28.    }

  29. }

通过jstack分析死锁

  1. 首先通过jps获取当前运行的进程的pid

  1. 16628Jps

  2. 17588RemoteMavenServer

  3. 19220Launcher

  4. 19004DeadLockDemo

  2. jstack打印堆栈信息,输入 jstack19004, 会打印如下日志,可以很明显看到死锁的信息提示

  1. Found one Java-level deadlock:

  2. =============================

  3. "Thread-1":

  4.  waiting to lock monitor 0x000000001d461e68(object0x000000076b310df8, a java.lang.Object),

  5.  which is held by"Thread-0"

  6. "Thread-0":

  7.  waiting to lock monitor 0x000000001d463258(object0x000000076b310e08, a java.lang.Object),

  8.  which is held by"Thread-1"

解决死锁的手段

  • 保证多个线程按照相同的顺序获取锁

  • 设置获取锁的超时时间,超过设定时间以后自动释放

  • 死锁检测

资源限制

资源限制主要指的是硬件资源和软件资源,在开发多线程应用时,程序的执行速度受限于这两个资源。硬件的资源限制无非就是磁盘、CPU、内存、网络;软件资源的限制有很多,比如数据库连接数、计算机能够支持的最大连接数等 资源限制导致的问题最直观的体现就是前面说的上下文切换,也就是CPU资源和线程资源的严重不均衡导致频繁上下文切换,反而会造成程序的运行速度下降

资源限制的主要解决方案,就是缺啥补啥。CPU不够用,可以增加CPU核心数;一台机器的资源有限,则增加多台机器来做集群。

02

线程在Java中的使用

在Java中实现多线程的方式比较简单,因为Java中提供了非常方便的API来实现多线程。

  • 继承Thread类实现多线程

  • 实现Runnable接口

  • 实现Callable接口通过Future包装器来创建Thread线程,这种是带返回值的线程

  • 使用线程池ExecutorService

继承Thread类

继承Thread类,然后重写run方法,在run方法中编写当前线程需要执行的逻辑。最后通过线程实例的start方法来启动一个线程

  1. publicclassThreadDemoextendsThread{

  2.    @Override

  3.    publicvoid run(){

  4.        //重写run方法,提供当前线程执行的逻辑

  5.        System.out.println("Hello world");

  6.    }

  7.    publicstaticvoid main(String[] args){

  8.        ThreadDemo threadDemo=newThreadDemo();

  9.        threadDemo.start();

  10.    }

  11. }

Thread类其实是实现了Runnable接口,因此Thread自己也是一个线程实例,但是我们不能直接用 newThread().start()去启动一个线程,原因很简单,Thread类中的run方法是没有实际意义的,只是一个调用通过构造函数传递寄来的另一个Runnable实现类的run方法,这块的具体演示会在Runnable接口的代码中看到

  1. public

  2. classThreadimplementsRunnable{

  3.    /* What will be run. */

  4.    privateRunnable target;

  5.    ...

  6.    @Override

  7.    publicvoid run(){

  8.        if(target !=null){

  9.            target.run();

  10.        }

  11.    }

  12.    ...

实现Runnable接口

如果需要使用线程的类已经继承了其他的类,那么按照Java的单一继承原则,无法再继承Thread类来实现线程,所以可以通过实现Runnable接口来实现多线程

  1. publicclassRunnableDemoimplementsRunnable{

  2.    @Override

  3.    publicvoid run(){

  4.        //重写run方法,提供当前线程执行的逻辑

  5.        System.out.println("Hello world");

  6.    }

  7.    publicstaticvoid main(String[] args){

  8.        RunnableDemo runnableDemo=newRunnableDemo();

  9.        newThread(runnableDemo).start();

  10.    }

  11. }

上面的代码中,实现了Runnable接口,重写了run方法;接着为了能够启动RunnableDemo这个线程,必须要实例化一个Thread类,通过构造方法传递一个Runnable接口实现类去启动,Thread的run方法就会调用target.run来运行当前线程,代码在上面.

实现Callable接口

在有些多线程使用的场景中,我们有时候需要获取异步线程执行完毕以后的反馈结果,也许是主线程需要拿到子线程的执行结果来处理其他业务逻辑,也许是需要知道线程执行的状态。那么Callable接口可以很好的实现这个功能

  1. publicclassCallableDemoimplementsCallable<String>{

  2.    @Override

  3.    publicString call()throwsException{

  4.        return"hello world";

  5.    }

  6.    publicstaticvoid main(String[] args)throwsExecutionException,InterruptedException{

  7.        Callable<String> callable=newCallableDemo();

  8.        FutureTask<String> task=newFutureTask<>(callable);

  9.        newThread(task).start();

  10.        System.out.println(task.get());//获取线程的返回值

  11.    }

  12. }

在上面代码案例中的最后一行 task.get()就是获取线程的返回值,这个过程是阻塞的,当子线程还没有执行完的时候,主线程会一直阻塞直到结果返回

使用线程池

为了减少频繁创建线程和销毁线程带来的性能开销,在实际使用的时候我们会采用线程池来创建线程,在这里我不打算展开多线程的好处和原理,我会在后续的文章中单独说明。

  1. publicclassExecutorServiceDemo{

  2.    publicstaticvoid main(String[] args)throwsExecutionException,InterruptedException{

  3.        //创建一个固定线程数的线程池

  4.        ExecutorService pool =Executors.newFixedThreadPool(1);

  5.        Future future=pool.submit(newCallableDemo());

  6.        System.out.println(future.get());

  7.    }

  8. }

pool.submit有几个重载方法,可以传递带返回值的线程实例,也可以传递不带返回值的线程实例,源代码如下

  1. /*01*/Future<?> submit(Runnable task);

  2. /*02*/<T>Future<T> submit(Runnable task, T result);

  3. /*03*/<T>Future<T> submit(Callable<T> task);

 

 

线程的启动原理

 

前面我们简单分析过了线程的使用,通过调用线程的start方法来启动线程,线程启动后会调用run方法执行业务逻辑,run方法执行完毕后,线程的生命周期也就终止了。 很多同学最早学习线程的时候会比较疑惑,启动一个线程为什么是调用start方法,而不是run方法,这做一个简单的分析,先简单看一下start方法的定义

  1. publicclassThreadimplementsRunnable{

  2. ...

  3. publicsynchronizedvoid start(){

  4.        /**

  5.         * This method is not invoked for the main method thread or "system"

  6.         * group threads created/set up by the VM. Any new functionality added

  7.         * to this method in the future may have to also be added to the VM.

  8.         *

  9.         * A zero status value corresponds to state "NEW".

  10.         */

  11.        if(threadStatus !=0)

  12.            thrownewIllegalThreadStateException();

  13.        /* Notify the group that this thread is about to be started

  14.         * so that it can be added to the group's list of threads

  15.         * and the group's unstarted count can be decremented. */

  16.        group.add(this);

  17.        boolean started =false;

  18.        try{

  19.            start0();//注意这里

  20.            started =true;

  21.        }finally{

  22.            try{

  23.                if(!started){

  24.                    group.threadStartFailed(this);

  25.                }

  26.            }catch(Throwable ignore){

  27.                /* do nothing. If start0 threw a Throwable then

  28.                  it will be passed up the call stack */

  29.            }

  30.        }

  31.    }

  32.    privatenativevoid start0();//注意这里

  33. ...

我们看到调用start方法实际上是调用一个native方法start0()来启动一个线程,首先start0()这个方法是在Thread的静态块中来注册的,代码如下

  1. publicclassThreadimplementsRunnable{

  2.    /* Make sure registerNatives is the first thing <clinit> does. */

  3.    privatestaticnativevoid registerNatives();

  4.    static{

  5.        registerNatives();

  6.    }

这个registerNatives的作用是注册一些本地方法提供给Thread类来使用,比如start0()、isAlive()、currentThread()、sleep();这些都是大家很熟悉的方法。 registerNatives的本地方法的定义在文件 Thread.cThread.c定义了各个操作系统平台要用的关于线程的公共数据和操作,以下是Thread.c的全部内容

  1. staticJNINativeMethod methods[]={

  2.    {"start0",           "()V",        (void*)&JVM_StartThread},

  3.    {"stop0",            "(" OBJ ")V",(void*)&JVM_StopThread},

  4.    {"isAlive",          "()Z",        (void*)&JVM_IsThreadAlive},

  5.    {"suspend0",         "()V",        (void*)&JVM_SuspendThread},

  6.    {"resume0",          "()V",        (void*)&JVM_ResumeThread},

  7.    {"setPriority0",     "(I)V",       (void*)&JVM_SetThreadPriority},

  8.    {"yield",            "()V",        (void*)&JVM_Yield},

  9.    {"sleep",            "(J)V",       (void*)&JVM_Sleep},

  10.    {"currentThread",    "()" THD,     (void*)&JVM_CurrentThread},

  11.    {"countStackFrames","()I",        (void*)&JVM_CountStackFrames},

  12.    {"interrupt0",       "()V",        (void*)&JVM_Interrupt},

  13.    {"isInterrupted",    "(Z)Z",       (void*)&JVM_IsInterrupted},

  14.    {"holdsLock",        "(" OBJ ")Z",(void*)&JVM_HoldsLock},

  15.    {"getThreads",        "()[" THD,   (void*)&JVM_GetAllThreads},

  16.    {"dumpThreads",      "([" THD ")[[" STE,(void*)&JVM_DumpThreads},

  17.    {"setNativeName",    "(" STR ")V",(void*)&JVM_SetNativeThreadName},

  18. };

  19. #undef THD

  20. #undef OBJ

  21. #undef STE

  22. #undef STR

  23. JNIEXPORT void JNICALL

  24. Java_java_lang_Thread_registerNatives(JNIEnv*env, jclass cls)

  25. {

  26.    (*env)->RegisterNatives(env, cls, methods, ARRAY_LENGTH(methods));

  27. }

从这段代码可以看出,start0(),实际会执行 JVM_StartThread方法,这个方法是干嘛的呢? 从名字上来看,似乎是在JVM层面去启动一个线程,如果真的是这样,那么在JVM层面,一定会调用Java中定义的run方法。那接下来继续去找找答案。我们找到 jvm.cpp这个文件;这个文件需要下载hotspot的源码才能找到.

  1. JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))

  2.  JVMWrapper("JVM_StartThread");

  3. ...

  4. native_thread =newJavaThread(&thread_entry, sz);

  5. ...

JVM_ENTRY是用来定义 JVM_StartThread函数的,在这个函数里面创建了一个真正和平台有关的本地线程. 本着打破砂锅查到底的原则,继续看看 newJavaThread做了什么事情,继续寻找JavaThread的定义 在hotspot的源码中 thread.cpp文件中1558行的位置可以找到如下代码

  1. JavaThread::JavaThread(ThreadFunction entry_point,size_t stack_sz):

  2.  Thread()

  3. #if INCLUDE_ALL_GCS

  4.  , _satb_mark_queue(&_satb_mark_queue_set),

  5.  _dirty_card_queue(&_dirty_card_queue_set)

  6. #endif// INCLUDE_ALL_GCS

  7. {

  8.  if(TraceThreadEvents){

  9.    tty->print_cr("creating thread %p",this);

  10.  }

  11.  initialize();

  12.  _jni_attach_state = _not_attaching_via_jni;

  13.  set_entry_point(entry_point);

  14.  // Create the native thread itself.

  15.  // %note runtime_23

  16.  os::ThreadType thr_type = os::java_thread;

  17.  thr_type = entry_point ==&compiler_thread_entry ? os::compiler_thread :

  18.                                                     os::java_thread;

  19.  os::create_thread(this, thr_type, stack_sz);

  20.  _safepoint_visible =false;

  21.  // The _osthread may be NULL here because we ran out of memory (too many threads active).

  22.  // We need to throw and OutOfMemoryError - however we cannot do this here because the caller

  23.  // may hold a lock and all locks must be unlocked before throwing the exception (throwing

  24.  // the exception consists of creating the exception object & initializing it, initialization

  25.  // will leave the VM via a JavaCall and then all locks must be unlocked).

  26.  //

  27.  // The thread is still suspended when we reach here. Thread must be explicit started

  28.  // by creator! Furthermore, the thread must also explicitly be added to the Threads list

  29.  // by calling Threads:add. The reason why this is not done here, is because the thread

  30.  // object must be fully initialized (take a look at JVM_Start)

  31. }

这个方法有两个参数,第一个是函数名称,线程创建成功之后会根据这个函数名称调用对应的函数;第二个是当前进程内已经有的线程数量。最后我们重点关注与一下 os::create_thread,实际就是调用平台创建线程的方法来创建线程。 接下来就是线程的启动,会调用Thread.cpp文件中的Thread::start(Thread* thread)方法,代码如下

  1. voidThread::start(Thread* thread){

  2.  trace("start", thread);

  3.  // Start is different from resume in that its safety is guaranteed by context or

  4.  // being called from a Java method synchronized on the Thread object.

  5.  if(!DisableStartThread){

  6.    if(thread->is_Java_thread()){

  7.      // Initialize the thread state to RUNNABLE before starting this thread.

  8.      // Can not set it after the thread started because we do not know the

  9.      // exact thread state at that time. It could be in MONITOR_WAIT or

  10.      // in SLEEPING or some other state.

  11.      java_lang_Thread::set_thread_status(((JavaThread*)thread)->threadObj(),

  12.                                          java_lang_Thread::RUNNABLE);

  13.    }

  14.    os::start_thread(thread);

  15.  }

  16. }

start方法中有一个函数调用: os::start_thread(thread);,调用平台启动线程的方法,最终会调用Thread.cpp文件中的JavaThread::run()方法

  1. // The first routine called by a new Java thread

  2. voidJavaThread::run(){

  3.  // initialize thread-local alloc buffer related fields

  4.  this->initialize_tlab();

  5.  // used to test validitity of stack trace backs

  6.  this->record_base_of_stack_pointer();

  7.  // Record real stack base and size.

  8.  this->record_stack_base_and_size();

  9.  // Initialize thread local storage; set before calling MutexLocker

  10.  this->initialize_thread_local_storage();

  11.  this->create_stack_guard_pages();

  12.  this->cache_global_variables();

  13.  // Thread is now sufficient initialized to be handled by the safepoint code as being

  14.  // in the VM. Change thread state from _thread_new to _thread_in_vm

  15.  ThreadStateTransition::transition_and_fence(this, _thread_new, _thread_in_vm);

  16.  assert(JavaThread::current()==this,"sanity check");

  17.  assert(!Thread::current()->owns_locks(),"sanity check");

  18.  DTRACE_THREAD_PROBE(start,this);

  19.  // This operation might block. We call that after all safepoint checks for a new thread has

  20.  // been completed.

  21.  this->set_active_handles(JNIHandleBlock::allocate_block());

  22.  if(JvmtiExport::should_post_thread_life()){

  23.    JvmtiExport::post_thread_start(this);

  24.  }

  25.  EventThreadStartevent;

  26.  if(event.should_commit()){

  27.     event.set_javalangthread(java_lang_Thread::thread_id(this->threadObj()));

  28.     event.commit();

  29.  }

  30.  // We call another function to do the rest so we are sure that the stack addresses used

  31.  // from there will be lower than the stack base just computed

  32.  thread_main_inner();

  33.  // Note, thread is no longer valid at this point!

  34. }

这个方法中主要是做一系列的初始化操作,最后有一个方法 thread_main_inner, 接下来看看这个方法的逻辑是什么样的

  1. voidJavaThread::thread_main_inner(){

  2.  assert(JavaThread::current()==this,"sanity check");

  3.  assert(this->threadObj()!= NULL,"just checking");

  4.  // Execute thread entry point unless this thread has a pending exception

  5.  // or has been stopped before starting.

  6.  // Note: Due to JVM_StopThread we can have pending exceptions already!

  7.  if(!this->has_pending_exception()&&

  8.      !java_lang_Thread::is_stillborn(this->threadObj())){

  9.    {

  10.      ResourceMark rm(this);

  11.      this->set_native_thread_name(this->get_thread_name());

  12.    }

  13.    HandleMark hm(this);

  14.    this->entry_point()(this,this);

  15.  }

  16.  DTRACE_THREAD_PROBE(stop,this);

  17.  this->exit(false);

  18.  deletethis;

  19. }

和主流程无关的代码咱们先不去看,直接找到最核心的代码块 this->entry_point()(this,this);, 这个entrypoint应该比较熟悉了,因为我们在前面提到了,在::JavaThread这个方法中传递的第一个参数,代表函数名称,线程启动的时候会调用这个函数。 如果大家还没有晕车的话,应该记得我们在jvm.cpp文件中看到的代码,在创建 native_thread=newJavaThread(&thread_entry,sz); 的时候传递了一个threadentry函数,所以我们在jvm.cpp中找到这个函数的定义如下

  1. staticvoid thread_entry(JavaThread* thread, TRAPS){

  2.  HandleMark hm(THREAD);

  3.  Handle obj(THREAD, thread->threadObj());

  4.  JavaValue result(T_VOID);

  5.  JavaCalls::call_virtual(&result,

  6.                          obj,

  7.                          KlassHandle(THREAD,SystemDictionary::Thread_klass()),

  8.                          vmSymbols::run_method_name(),//注意这里

  9.                          vmSymbols::void_method_signature(),

  10.                          THREAD);

  11. }

可以看到 vmSymbols::run_method_name()这个调用,其实就是通过回调方法调用Java线程中定义的run方法, run_method_name是一个宏定义,在vmSymbols.hpp文件中可以找到如下代码

  1. #define VM_SYMBOLS_DO(template, do_alias)    

  2. ...

  3. template(run_method_name,"run")  

  4. ...

 

所以结论就是,Java里面创建线程之后必须要调用start方法才能真正的创建一个线程,该方法会调用虚拟机启动一个本地线程,本地线程的创建会调用当前系统创建线程的方法进行创建,并且线程被执行的时候会回调 run方法进行业务逻辑的处理

02

线程的终止方法及原理

线程的终止有主动和被动之分,被动表示线程出现异常退出或者run方法执行完毕,线程会自动终止。主动的方式是 Thread.stop()来实现线程的终止,但是stop()方法是一个过期的方法,官方是不建议使用,理由很简单,stop()方法在中介一个线程时不会保证线程的资源正常释放,也就是不会给线程完成资源释放工作的机会,相当于我们在linux上通过kill -9强制结束一个进程。

那么如何安全的终止一个线程呢?

我们先看一下下面的代码,代码演示了一个正确终止线程的方法,至于它的实现原理,稍后我们再分析

  1. publicclassInterruptedDemoimplementsRunnable{

  2.    @Override

  3.    publicvoid run(){

  4.        long i=0l;

  5.        while(!Thread.currentThread().isInterrupted()){//notice here

  6.            i++;

  7.        }

  8.        System.out.println("result:"+i);

  9.    }

  10.    publicstaticvoid main(String[] args)throwsInterruptedException{

  11.        InterruptedDemo interruptedDemo=newInterruptedDemo();

  12.        Thread thread=newThread(interruptedDemo);

  13.        thread.start();

  14.        Thread.sleep(1000);//睡眠一秒

  15.        thread.interrupt();//notice here

  16.    }

  17. }

代码中有两处需要注意,在main线程中,调用了线程的interrupt()方法、在run方法中,while循环中通过 Thread.currentThread().isInterrupted()来判断线程中断的标识。所以我们在这里猜想一下,应该是在线程中维护了一个中断标识,通过 thread.interrupt()方法去改变了中断标识的值使得run方法中while循环的判断不成立而跳出循环,因此run方法执行完毕以后线程就终止了。

线程中断的原理分析

我们来看一下 thread.interrupt()方法做了什么事情

  1. publicclassThreadimplementsRunnable{

  2. ...

  3.    publicvoid interrupt(){

  4.        if(this!=Thread.currentThread())

  5.            checkAccess();

  6.        synchronized(blockerLock){

  7.            Interruptible b = blocker;

  8.            if(b !=null){

  9.                interrupt0();           // Just to set the interrupt flag

  10.                b.interrupt(this);

  11.                return;

  12.            }

  13.        }

  14.        interrupt0();

  15.    }

  16. ...

这个方法里面,调用了interrupt0(),这个方法在前面分析start方法的时候见过,是一个native方法,这里就不再重复贴代码了,同样,我们找到jvm.cpp文件,找到JVM_Interrupt的定义

  1. JVM_ENTRY(void, JVM_Interrupt(JNIEnv* env, jobject jthread))

  2.  JVMWrapper("JVM_Interrupt");

  3.  // Ensure that the C++ Thread and OSThread structures aren't freed before we operate

  4.  oop java_thread =JNIHandles::resolve_non_null(jthread);

  5.  MutexLockerEx ml(thread->threadObj()== java_thread ? NULL :Threads_lock);

  6.  // We need to re-resolve the java_thread, since a GC might have happened during the

  7.  // acquire of the lock

  8.  JavaThread* thr = java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread));

  9.  if(thr != NULL){

  10.    Thread::interrupt(thr);

  11.  }

  12. JVM_END

这个方法比较简单,直接调用了 Thread::interrupt(thr)这个方法,这个方法的定义在Thread.cpp文件中,代码如下

  1. voidThread::interrupt(Thread* thread){

  2.  trace("interrupt", thread);

  3.  debug_only(check_for_dangling_thread_pointer(thread);)

  4.  os::interrupt(thread);

  5. }

Thread::interrupt方法调用了os::interrupt方法,这个是调用平台的interrupt方法,这个方法的实现是在 os_*.cpp文件中,其中星号代表的是不同平台,因为jvm是跨平台的,所以对于不同的操作平台,线程的调度方式都是不一样的。我们以os_linux.cpp文件为例

  1. void os::interrupt(Thread* thread){

  2.  assert(Thread::current()== thread ||Threads_lock->owned_by_self(),

  3.    "possibility of dangling Thread pointer");

  4.  //获取本地线程对象

  5.  OSThread* osthread = thread->osthread();

  6.  if(!osthread->interrupted()){//判断本地线程对象是否为中断

  7.    osthread->set_interrupted(true);//设置中断状态为true

  8.    // More than one thread can get here with the same value of osthread,

  9.    // resulting in multiple notifications.  We do, however, want the store

  10.    // to interrupted() to be visible to other threads before we execute unpark().

  11.    //这里是内存屏障,这块在后续的文章中会剖析;内存屏障的目的是使得interrupted状态对其他线程立即可见

  12.    OrderAccess::fence();

  13.    //_SleepEvent相当于Thread.sleep,表示如果线程调用了sleep方法,则通过unpark唤醒

  14.    ParkEvent*const slp = thread->_SleepEvent;

  15.    if(slp != NULL) slp->unpark();

  16.  }

  17.  // For JSR166. Unpark even if interrupt status already was set

  18.  if(thread->is_Java_thread())

  19.    ((JavaThread*)thread)->parker()->unpark();

  20.  //_ParkEvent用于synchronized同步块和Object.wait(),这里相当于也是通过unpark进行唤醒

  21.  ParkEvent* ev = thread->_ParkEvent;

  22.  if(ev != NULL) ev->unpark();

  23. }

通过上面的代码分析可以知道,thread.interrupt()方法实际就是设置一个interrupted状态标识为true、并且通过ParkEvent的unpark方法来唤醒线程。

  • 对于synchronized阻塞的线程,被唤醒以后会继续尝试获取锁,如果失败仍然可能被park

  • 在调用ParkEvent的park方法之前,会先判断线程的中断状态,如果为true,会清除当前线程的中断标识

  • Object.wait、Thread.sleep、Thread.join会抛出InterruptedException

这里给大家普及一个知识点,为什么Object.wait、Thread.sleep和Thread.join都会抛出InterruptedException?首先,这个异常的意思是表示一个阻塞被其他线程中断了。然后,由于线程调用了interrupt()中断方法,那么Object.wait、Thread.sleep等被阻塞的线程被唤醒以后会通过is_interrupted方法判断中断标识的状态变化,如果发现中断标识为true,则先清除中断标识,然后抛出InterruptedException

需要注意的是,InterruptedException异常的抛出并不意味着线程必须终止,而是提醒当前线程有中断的操作发生,至于接下来怎么处理取决于线程本身,比如

  • 直接捕获异常不做任何处理

  • 将异常往外抛出

  • 停止当前线程,并打印异常信息

为了让大家能够更好的理解上面这段话,我们以Thread.sleep为例直接从jdk的源码中找到中断标识的清除以及异常抛出的方法代码

找到 is_interrupted()方法,linux平台中的实现在os_linux.cpp文件中,代码如下

  1. bool os::is_interrupted(Thread* thread,bool clear_interrupted){

  2.  assert(Thread::current()== thread ||Threads_lock->owned_by_self(),

  3.    "possibility of dangling Thread pointer");

  4.  OSThread* osthread = thread->osthread();

  5.  bool interrupted = osthread->interrupted();//获取线程的中断标识

  6.  if(interrupted && clear_interrupted){//如果中断标识为true

  7.    osthread->set_interrupted(false);//设置中断标识为false

  8.    // consider thread->_SleepEvent->reset() ... optional optimization

  9.  }

  10.  return interrupted;

  11. }

找到Thread.sleep这个操作在jdk中的源码体现,怎么找?相信如果前面大家有认真看的话,应该能很快找到,代码在jvm.cpp文件中

  1. JVM_ENTRY(void, JVM_Sleep(JNIEnv* env, jclass threadClass, jlong millis))

  2.  JVMWrapper("JVM_Sleep");

  3.  if(millis <0){

  4.    THROW_MSG(vmSymbols::java_lang_IllegalArgumentException(),"timeout value is negative");

  5.  }

  6.  //判断并清除线程中断状态,如果中断状态为true,抛出中断异常

  7.  if(Thread::is_interrupted (THREAD,true)&&!HAS_PENDING_EXCEPTION){

  8.    THROW_MSG(vmSymbols::java_lang_InterruptedException(),"sleep interrupted");

  9.  }

  10.  // Save current thread state and restore it at the end of this block.

  11.  // And set new thread state to SLEEPING.

  12.  JavaThreadSleepState jtss(thread);

  13. ...

注意上面加了中文注释的地方的代码,先判断is_interrupted的状态,然后抛出一个InterruptedException异常。到此为止,我们就已经分析清楚了中断的整个流程。

Java线程的中断标识判断

了解了thread.interrupt方法的作用以后,再回过头来看Java中 Thread.currentThread().isInterrupted()这段代码,就很好理解了。由于前者先设置了一个中断标识为true,所以 isInterrupted()这个方法的返回值为true,故而不满足while循环的判断条件导致退出循环。 这里有必要再提一句,就是这个线程中断标识有两种方式复位,第一种是前面提到过的InterruptedException;另一种是通过Thread.interrupted()对当前线程的中断标识进行复位。

 

 

from

https://mp.weixin.qq.com/s?__biz=MzUxNDA1NDI3OA==&mid=2247484980&idx=1&sn=1c5fba57f79a696bd467e5d5d3aa12e7&chksm=f94a87ddce3d0ecb4e58066607d64ab7f3be36db398a79b3828449a4cc0185384815cfa4274d&mpshare=1&scene=1&srcid=1204qe1gtGwG3PRoAqMFBcfn#rd

 

https://mp.weixin.qq.com/s?__biz=MzUxNDA1NDI3OA==&mid=2247484980&idx=2&sn=f53f02c719130ceb2ea776f28356f9d3&chksm=f94a87ddce3d0ecbd4e3155e4430a1d972017ff06c480eb30da21b02d3b753ce09c48df34ba2&mpshare=1&scene=1&srcid=1205rkjB8HNDe9by1EGZm8ee#rd

 

https://mp.weixin.qq.com/s?__biz=MzUxNDA1NDI3OA==&mid=2247484980&idx=3&sn=2826ad9b3481ae18ee311cbcee195ca7&chksm=f94a87ddce3d0ecb988997d6ea64722fe386e749077bdd0712833c899514cbc81a0b63240a5f&mpshare=1&scene=1&srcid=1205KnclzuczOcT03pAYcq0G#rd

分享到:
评论

相关推荐

    Linux下的多线程编程.pdf

    1. 多线程技术的历史和发展: 多线程技术最初在60年代被提出,但直到80年代才开始被广泛应用于操作系统中。solaris系统是使用多线程的先行者,传统Unix系统虽然支持线程概念,但是早期版本中的进程只有一个线程,...

    嵌入式Linux应用程序开发详解-第9章(多线程编程).pdf

    通过深入理解线程的基本概念、分类和发展历史,以及掌握线程的创建、管理、属性设置等关键技能,开发者可以构建高性能、可靠的多线程应用程序。这对于从事嵌入式开发的专业人士来说尤为重要,因为多线程技术能够极大...

    利用MIPS多线程处理器优化SoC设计

    在半导体产业中,系统级芯片(SoC)的设计是一个多学科交叉的复杂过程,它融合了处理器、内存、输入...在今天的高性能计算领域,MIPS多线程技术已经成为实现最佳性能的有效工具,并在不断演进中展现出强大的发展潜力。

    英特尔多核多线程技术(中文版带书签)

    ### 英特尔多核多线程技术概览 #### 计算机发展历程与微处理器技术 ...未来,随着人工智能、大数据等新兴技术的发展,对于计算性能的需求将进一步提高,这也意味着多核多线程技术将继续发挥着至关重要的作用。

    C++多线程编程

    ##### 进程与线程的历史背景 早期计算机系统设计时,由于硬件资源限制,一个程序独占整个系统的资源进行运行是非常普遍的做法。随着技术的发展,特别是大型机时代的到来,对资源的高效利用提出了更高的需求。因此,...

    C#__多线程

    随着计算硬件的不断发展和计算机性能的需求日益增长,多线程编程已经成为现代软件开发不可或缺的一部分。 ##### 1.1 多线程的历史背景 早期的计算硬件虽然复杂,但操作系统执行的功能却相对简单。操作系统在同一...

    OpenMP多线程并行编程资料

    - **历史**:OpenMP始于1997年,由Intel、IBM、SUN等公司发起,旨在简化共享内存多处理器系统的并行编程。 - **版本演进**:从最初的1.0版本到现在最新的版本,OpenMP不断引入新特性以适应现代硬件的发展需求。 - **...

    并发编程实践,全面介绍基础知识、JVM同步原语、线程安全、低级并发工具、线程安全容器、高级线程协作工具、Executor部分等

    - **Java并发演进历史**:从Java 1.0的Thread类到Java 5引入的并发包`java.util.concurrent`,再到Java 7和8的改进,Java并发API不断发展,提供了更多高效、易用的并发工具。 - **Java并发模型**:Java并发模型...

    JAVA教程之线程篇

    #### 线程的重要性及历史背景 在Java的世界里,线程不再是一项边缘技术,而是编程的核心组成部分。自Java诞生之初,线程的概念就被深深植根于其架构中,这标志着Java在设计时便充分考虑了并发处理的需求。线程的...

    利用MIPS多线程处理器优化SoC设计.pdf

    虽然软件多线程技术如任务切换和基于软件的线程调度早已存在,但硬件多线程技术的历史同样悠久,可以追溯至20世纪60年代的CDC6600计算机,其中使用了10个硬件线程来优化I/O处理器对多个外围设备的响应时间。...

    一种在处理器中挂起和释放执行过程中计算线程的整体机制的制作方法.pdf

    数字计算的发展历史显示了在各方面都有持续的进步。持续的进步一直在发生,例如处理器的装置密度与线路互连的技术,可用于改善运算速度、容错能力、使用更高速的时脉信号或者更多其它改进。并行处理的概念包括将任务...

    C语言的并行魔法:多线程编程实现指南

    随着计算机硬件的发展,特别是多核处理器的普及,多线程编程成为了提升程序性能的关键技术之一。本文将深入探讨如何在C语言中实现多线程编程,并结合POSIX线程库(pthread)进行详细讲解。 #### 多线程编程的重要性...

    cln.rar_进程与线程

    4. History.txt:可能包含了组件的更新历史或开发日志,对于理解组件的发展和变更有帮助。 5. CoolTrayIcon.~pas:这是一个临时文件,可能是Delphi编辑器在编辑源代码时生成的,通常不包含最终编译的信息,但有时...

    01-计算机发展历史1

    计算机发展历史 自19世纪末到21世纪初,计算机的发展经历了多个阶段,每一步都带来了技术的巨大飞跃。在这一过程中,编程语言也扮演了至关重要的角色,Java作为其中的一种,更是影响深远。 1. 计算机的起源:机械...

    同时多线程处理器前端系统的研究的开题报告.pdf

    通过深入研究这些文献,可以深入了解多线程处理器的历史发展、技术现状及未来趋势。 总的来说,这份开题报告为深入理解多线程处理器前端系统及其优化提供了全面的框架,旨在推动计算机系统性能的持续提升,对于...

    MAC OS 系统的发展历史.

    ### MAC OS 系统的发展历史 #### 一、引言 Mac OS,即苹果公司的操作系统,自1984年首次推出以来,一直是苹果Macintosh系列电脑的核心组成部分。这款操作系统以其创新的图形用户界面(GUI)而闻名,是最早在商业上...

    JVM历史发展和内存回收笔记.rar

    **JVM历史发展** Java虚拟机(JVM)自1995年随着Java语言的发布而诞生,至今已有二十多年的历史。它的发展历程可以分为以下几个主要阶段: 1. **早期版本**:JVM的首个公开版本是1.0,那时的JVM执行效率较低,主要...

    redis-发展历史及基础知识

    ### Redis 发展历史及基础知识详解 #### 一、Redis 历程概览 Redis (Remote Dictionary Server) 是一个开源的、支持多种数据结构的内存数据存储系统。它以其高性能和灵活性著称,在缓存、消息队列以及实时数据分析...

Global site tag (gtag.js) - Google Analytics