• 彻底理解线程


    🚀 优质资源分享 🚀

    学习路线指引(点击解锁)知识定位人群定位
    🧡 Python实战微信订餐小程序 🧡进阶级本课程是python flask+微信小程序的完美结合,从项目搭建到腾讯云部署上线,打造一个全栈订餐系统。
    💛Python量化交易实战💛入门级手把手带你打造一个易扩展、更安全、效率更高的量化交易系统
    1 线程的意义

    操作系统支持多个应用程序同时执行,每个应用至少对应一个进程,彼此之间的操作和数据不受干扰。当一个进程需要磁盘IO的时候,CPU就切换到另外的进程,提高了CPU利用率。

    有了进程,为什么还要线程?因为进程的成本太高了。

    启动新的进程必须分配独立的内存空间,建立数据表维护它的代码段、堆栈段和数据段,这是昂贵的多任务工作方式。如果两个进程之间需要通信,要采用管道通信、消息队列、共享内存等等方式。线程可以看作轻量化的进程,或者粒度更小的进程。线程之间使用相同的地址空间,切换线程的时间远远小于切换进程的时间。一个进程的开销大约是线程开销的30倍左右。

    随着操作系统的发展,进程已经演变成了线程容器的角色。进程是资源分配的最小单位,线程是CPU调度的最小单位。每一个进程中至少有一个线程,同一进程的所有线程共享该进程的所有资源。

    2 详解Java线程

    我们以Java语言和JVM为例,了解一下线程的实现原理。

    2.1 线程的底层实现

    启动一个Java程序会创建一个JVM进程,JVM创建、管理线程本质都是调用操作系统接口。

    public class TestThreadStart {
        public static void main(String[] args) throws InterruptedException {
            Thread thread = new Thread(() -> {
                System.out.println("start thread now ");
            }, "TestThreadStart");
            thread.run();
            System.out.println("the state of thread is " + thread.getState().name());
            thread.start();
            System.out.println("the state of thread is " + thread.getState().name());
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    以上代码演示了使用start方法启动线程,run方法只是执行同步方法,输出结果如下:

    start thread now 
    the state of thread is NEW
    the state of thread is RUNNABLE
    start thread now 
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    JVM源码文件 https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/java/lang/Thread.java 中,可以看到线程启动的start方法调用本地方法start0。

        public void start() {
            synchronized (this) {
                // zero status corresponds to state "NEW".
                if (holder.threadStatus != 0)
                    throw new IllegalThreadStateException();
                start0();
            }
        }
        
        private native void start0();
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    源码文件 https://github.com/openjdk/jdk/blob/master/src/java.base/share/native/libjava/Thread.c 中,实现了start0方法映射到JVM本地方法。

    static JNINativeMethod methods[] = {
        {"start0",           "()V",        (void *)&JVM_StartThread},
        {"stop0",            "(" OBJ ")V", (void *)&JVM_StopThread},
        {"isAlive0",         "()Z",        (void *)&JVM_IsThreadAlive},
        {"suspend0",         "()V",        (void *)&JVM_SuspendThread},
        {"resume0",          "()V",        (void *)&JVM_ResumeThread},
        {"setPriority0",     "(I)V",       (void *)&JVM_SetThreadPriority},
        {"yield0",           "()V",        (void *)&JVM_Yield},
        {"sleep0",           "(J)V",       (void *)&JVM_Sleep},
        {"currentCarrierThread", "()" THD, (void *)&JVM_CurrentCarrierThread},
        {"currentThread",    "()" THD,     (void *)&JVM_CurrentThread},
        {"setCurrentThread", "(" THD ")V", (void *)&JVM_SetCurrentThread},
        {"interrupt0",       "()V",        (void *)&JVM_Interrupt},
        {"holdsLock",        "(" OBJ ")Z", (void *)&JVM_HoldsLock},
        {"getThreads",       "()[" THD,    (void *)&JVM_GetAllThreads},
        {"dumpThreads",      "([" THD ")[[" STE, (void *)&JVM_DumpThreads},
        {"getStackTrace0",   "()" OBJ,     (void *)&JVM_GetStackTrace},
        {"setNativeName",    "(" STR ")V", (void *)&JVM_SetNativeThreadName},
        {"extentLocalCache",  "()[" OBJ,    (void *)&JVM_ExtentLocalCache},
        {"setExtentLocalCache", "([" OBJ ")V",(void *)&JVM_SetExtentLocalCache},
        {"getNextThreadIdOffset", "()J",     (void *)&JVM_GetNextThreadIdOffset}
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    在源码文件 https://github.com/openjdk/jdk/blob/master/src/hotspot/share/runtime/thread.cpp 可以看到启动线程依赖系统级方法os::start_thread(thread)

    void Thread::start(Thread* thread) {
      // Start is different from resume in that its safety is guaranteed by context or
      // being called from a Java method synchronized on the Thread object.
      if (thread->is_Java_thread()) {
        // Initialize the thread state to RUNNABLE before starting this thread.
        // Can not set it after the thread started because we do not know the
        // exact thread state at that time. It could be in MONITOR\_WAIT or
        // in SLEEPING or some other state.
        java_lang_Thread::set_thread_status(JavaThread::cast(thread)->threadObj(),
                                            JavaThreadStatus::RUNNABLE);
      }
      os::start_thread(thread);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在源码文件 https://github.com/openjdk/jdk/blob/master/src/hotspot/share/runtime/os.cpp 找到os::start_thread方法,可以看到系统创建了线程,并且状态设置为RUNNABLE。

    void os::start\_thread(Thread* thread) {
      OSThread* osthread = thread->osthread();
      osthread->set_state(RUNNABLE);
      pd_start_thread(thread);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    Linux系统并没有把线程和进程区别对待,无论线程还是进程都是一个数据结构,用task_struct结构体表示,唯一的区别是共享的数据区域不同。

    struct task\_struct {
        // 进程状态
        long              state;
        // 虚拟内存结构体
        struct mm\_struct *mm;
        // 唯一进程号
        pid\_t             pid;
        // 指向父进程的指针
        struct task\_struct *parent;
        // 子进程列表
        struct list\_head children;
        // 存放文件系统信息的指针
        struct fs\_struct *fs;
        // 进程/线程打开的文件指针
        struct files\_struct *files;
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    以上代码是 task_struct 的极少部分字段。mm_struct是进程的虚拟内存空间,files_struct是进程将要读写的文件。Linux系统将一切外设和磁盘文件都当做文件处理,files_struct代表所有的IO操作。


    从上图可以看到,Linux创建进程和子进程会申请不同的内存空间,读写不同的文件;创建进程和进程下的线程,共享了内存空间,读写一样的文件。因此多线程应用程序要利用锁机制,避免在同一区域写入错乱数据的问题。

    2.2 线程的生命周期

    操作系统的线程生命周期可以分为五种状态。分别是:初始状态、可运行状态、运行状态、休眠状态和终止状态。JVM将线程等待状态细分成两种,一共六种状态。

    • NEW:创建。
    • RUNNABLE:运行中。
    • BLOCKED:受阻塞并等待某个监视器锁。
    • WAITING:无限期地等待。
    • TIMED_WAITING:等待指定时间。
    • TERMINATED:终止。
    2.3 线程的优先级

    操作系统调度线程有两种方式:

    • 协作式调度:当前线程完全占用CPU时间,执行时间由线程本身控制,直到运行结束,系统才执行下一个线程。可能出现一个线程一直占有CPU,而其他线程等待,导致整个系统崩溃。
    • 抢占式调度:操作系统决定下一个占用CPU时间的是哪一个线程,定期的中断当前正在执行的线程,任何一个线程都不能独占。不会因为一个线程而影响整个进程的执行,但是频繁阻塞和调度会造成系统资源的浪费。

    JVM的线程调度默认是抢占式调度,线程调度器按照优先级决定调度哪个线程来执行。线程优先级的范围是1~10,默认的优先级是5,10极最高。线程优先级高的不一定先执行,优先级低只是获得调度的概率低,并不是一定最后被调度。通过setPriority()可以改变线程优先级。

    2.4 JVM守护线程

    守护线程是一种JVM中特殊的线程,在后台完成一些系统性的服务,比如垃圾回收。应用程序创建的线程叫做用户线程,完成具体的业务操作。程序中所有的用户线程执行完毕之后,不管守护线程是否结束,JVM都会自动结束。任何线程都可以通过setDaemon()设置为守护线程和用户线程,如下代码所示:

    public class DaemonThreadDemo {
    
        public static void main(String[] args) {
            System.out.println("--主线程开始--");
            Thread thread = new Thread(() -> {
                while (true) {
                    System.out.println("执行守护线程");
                }
            });
            thread.setDaemon(true);
            thread.start();
            System.out.println("--主线程结束--");
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    程序运行结果:

    --主线程开始--
    --主线程结束--
    执行守护线程
    执行守护线程
    执行守护线程
    执行守护线程
    执行守护线程
    Process finished with exit code 0
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    当一个应用程序需要在后台持续做某件事情,就是守护线程的典型应用场景。比如开发一款社交软件,开启守护线程持续监听聊天消息。当应用程序退出时,守护线程一定会终止。

    参考文章:https://www.codingbrick.com/archives/937.html

  • 相关阅读:
    C++ Qt QString类用法介绍
    Java @PreDestroy 注解的使用
    数据中心网络方案设计
    STL库->String(C++)
    谈谈互联网免费思维
    nc反弹以及中 &>、0>&1是什么意思
    轻松搭建个人web站点:OpenWRT教程结合内网穿透技术实现公网远程访问
    自定义 HandlerMethodArgumentResolver 怎么和默认HandlerMethodArgumentResolver进行隔离的?
    【剑指Offer】16.数值的整数次方
    NIO与BIO服务器端对比
  • 原文地址:https://blog.csdn.net/m0_56069948/article/details/126564777