3

彻底理解线程 - 编码砖家

 1 year ago
source link: https://www.cnblogs.com/xiaoyangjia/p/16631830.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

彻底理解线程

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

以上代码演示了使用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操作。

31085-20220827235442631-1589634818.jpg
31085-20220827235434680-710706834.jpg

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

2.2 线程的生命周期

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

31085-20220827235459426-469660219.png
  • 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("--主线程结束--");
    }
}

程序运行结果:

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

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK