彻底理解线程

1 线程的意义

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

当您有一个进程时,为什么需要线程?因为这个过程的成本太高了。

[En]

Why do you need threads when you have a process? Because the cost of the process is too high.

启动一个新进程必须分配独立的内存空间并设置数据表来维护其代码、堆栈和数据段,这是一种昂贵的多任务工作方式。如果两个进程之间需要通信,则使用管道通信、消息队列、共享内存等。线程可以被认为是轻量级进程,或者是粒度较小的进程。线程之间使用相同的地址空间,切换线程的时间比切换进程的时间要少得多。一个进程的成本大约是一个线程的30倍。

[En]

Starting a new process must allocate independent memory space and set up data tables to maintain its code, stack, and data segments, which is an expensive multitasking way of working. If communication is needed between two processes, use pipeline communication, message queuing, shared memory, and so on. Threads can be thought of as lightweight processes, or processes with smaller granularity. The same address space is used between threads, and the time of switching threads is much less than that of switching processes. The cost of a process is about 30 times that of a thread.

随着操作系统的发展,进程已经演变成了线程容器的角色。进程是资源分配的最小单位,线程是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());
    }
}

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

start thread now
the state of thread is NEW
the state of thread is RUNNABLE
start thread now

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();

源码文件 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}
};

在源码文件 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);
}

在源码文件 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);
}

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;
};

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

彻底理解线程
彻底理解线程

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

2.2 线程的生命周期

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

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

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

[En]

There are two ways for the operating system to schedule threads:

  • 协作式调度:当前线程完全占用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("--主线程结束--");
    }
}

程序运行结果:

--主线程开始--
--主线程结束--
执行守护线程
执行守护线程
执行守护线程
执行守护线程
执行守护线程
Process finished with exit code 0

当应用程序需要在后台连续执行某些操作时,这是守护程序线程的典型应用程序场景。例如,开发一款社交软件,它可以打开守护程序线程,连续监听聊天消息。当应用程序退出时,守护程序线程必须终止。

[En]

When an application needs to do something continuously in the background, it is a typical application scenario for daemon threads. For example, develop a social software that turns on the daemon thread to listen for chat messages continuously. When the application exits, the daemon thread must terminate.

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

Original: https://www.cnblogs.com/xiaoyangjia/p/16631830.html
Author: 编码砖家
Title: 彻底理解线程

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/522809/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • kafka 常见面试题

    注入产生的原理: 数据库设置为GBK编码: 宽字节注入源于程序员设置MySQL连接时错误配置为:set character_set_client=gbk,这样配置会引发编码转换从而…

    大数据 2023年4月19日
    061
  • Redis 图形化桌面客户端

    注入产生的原理: 数据库设置为GBK编码: 宽字节注入源于程序员设置MySQL连接时错误配置为:set character_set_client=gbk,这样配置会引发编码转换从而…

    大数据 2023年2月14日
    074
  • 校园如何管理?且看可视化大屏

    [TencentCloudSDKException] code:FailedOperation.ServiceIsolate message:service is stopped …

    大数据 2023年6月2日
    064
  • Kafka入门实战教程(4):重要的集群参数配置

    1 如何规划Kafka 集群部署”兵马未动,粮草先行”,与其盲目上马一套Kafka环境然后事后费力调整,不如一开始就思考好实际场景下业务所需的集群环境。在考…

    大数据 2023年5月28日
    076
  • 数仓建模篇(一)

    注入产生的原理: 数据库设置为GBK编码: 宽字节注入源于程序员设置MySQL连接时错误配置为:set character_set_client=gbk,这样配置会引发编码转换从而…

    大数据 2023年4月27日
    051
  • 【Flink】详解Flink的八种分区

    注入产生的原理: 数据库设置为GBK编码: 宽字节注入源于程序员设置MySQL连接时错误配置为:set character_set_client=gbk,这样配置会引发编码转换从而…

    大数据 2023年4月23日
    0153
  • dask使用kerberos认证读取hive数据库

    注入产生的原理: 数据库设置为GBK编码: 宽字节注入源于程序员设置MySQL连接时错误配置为:set character_set_client=gbk,这样配置会引发编码转换从而…

    大数据 2023年2月11日
    0109
  • flink-cdc 实现oracle 实时同步到kudu

    注入产生的原理: 数据库设置为GBK编码: 宽字节注入源于程序员设置MySQL连接时错误配置为:set character_set_client=gbk,这样配置会引发编码转换从而…

    大数据 2023年4月22日
    067
  • Nacos配置的拉取以及热更新的实现

    注入产生的原理: 数据库设置为GBK编码: 宽字节注入源于程序员设置MySQL连接时错误配置为:set character_set_client=gbk,这样配置会引发编码转换从而…

    大数据 2023年3月2日
    080
  • dolphinscheduler 集成flink

    注入产生的原理: 数据库设置为GBK编码: 宽字节注入源于程序员设置MySQL连接时错误配置为:set character_set_client=gbk,这样配置会引发编码转换从而…

    大数据 2023年4月25日
    060
  • Spark编程题

    注入产生的原理: 数据库设置为GBK编码: 宽字节注入源于程序员设置MySQL连接时错误配置为:set character_set_client=gbk,这样配置会引发编码转换从而…

    大数据 2023年2月17日
    074
  • hive 数据仓库

    注入产生的原理: 数据库设置为GBK编码: 宽字节注入源于程序员设置MySQL连接时错误配置为:set character_set_client=gbk,这样配置会引发编码转换从而…

    大数据 2022年8月8日
    0187
  • springcloud整合zookeeper作为服务发现启动异常Error processing condition on org.springframework.boot.autoconfigure

    注入产生的原理: 数据库设置为GBK编码: 宽字节注入源于程序员设置MySQL连接时错误配置为:set character_set_client=gbk,这样配置会引发编码转换从而…

    大数据 2023年3月4日
    082
  • 使用二进制编译安装lamp (centos)

    软件包存放目录: 使用wget命令下载所需的编译包 注:查看是否有 /usr/local/mysql这个文件夹, 如果没有请创建这个文件夹 mkdir /usr/local/mys…

    大数据 2023年5月26日
    077
  • Mybatis-plus

    注入产生的原理: 数据库设置为GBK编码: 宽字节注入源于程序员设置MySQL连接时错误配置为:set character_set_client=gbk,这样配置会引发编码转换从而…

    大数据 2023年3月1日
    081
  • Hive SQL 高级函数使用

    注入产生的原理: 数据库设置为GBK编码: 宽字节注入源于程序员设置MySQL连接时错误配置为:set character_set_client=gbk,这样配置会引发编码转换从而…

    大数据 2023年2月12日
    0156
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球