今天聊聊线程(值得一看)

229 阅读6分钟

这是我参与8月更文挑战的第24天,活动详情查看:8月更文挑战

image.png

今天呢,我们聊聊最基础的一个点,也是必须要了解的一个点。聊正题之前 我想先向官方提个问题,为什么我的前两篇没有被推荐到首页,是货不够干吗,还是忽略我了,希望官方看到这个给我个答复,哈哈 开个玩笑。 今天呢 我们聊线程。我上一篇写了点线程安全的问题,欢迎大家阅读!

今天聊聊线程安全问题

线程有几种状态, 三种? 五种?

由于有些人看了我的上两篇感觉有些不太懂,所以今天这篇文章就出来了!(理解+代码

话不多说,进入正题

什么是线程

现代操作系统在运行一个程序时,会为其创建一个进程。例如,启动一个Java程序,操作系统就会创建一个Java进程。现代操作系统调度CPU的最小单元是线程,也叫轻量级进(Light Weight Process);

在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉到这些线程在同时执行。

线程的实现可以分为两类:

  • 用户级线程(User-Level Thread)

  • 内核级线程(Kernel-Level Thread)

在理解线程分类之前我们需要先了解系统的用户空间与内核空间两个概念,以4G大小的内存空间

image.png

看上图中,Linux为内核代码和数据结构预留了几个页框,这些页永远不会被转出到磁盘上。

从 0x00000000 到 0xc0000000(PAGE_OFFSET) 的线性地址可由用户代码 和 内核代码进行引用(即用户空间)。从0xc0000000(PAGE_OFFSET)到 0xFFFFFFFFF的线性地址只能由内核代码进行访问(即内核空间)。内核代码及其数据结构都必须位于这 1 GB的地址空间中,但是对于此地址空间而言,更大的消费者是物理地址的虚拟映射。

这意味着在 4 GB 的内存空间中,只有 3 GB 可以用于用户应用程序。一个进程只能运行在用户方式(usermode)或内核方式(kernelmode)下。用户程序运行在用户方式下,而系统调用运行在内核方式下。在这两种方式下所用的堆栈不一样:用户方式下用的是一般的堆栈,而内核方式下用的是固定大小的堆栈(一般为一个内存页的大小)

每个进程都有自己的 3 G 用户空间,它们共享1GB的内核空间。当一个进程从用户空间进入内核空间时,它就不再有自己的进程空间了。这也就是为什么我们经常说线程上下文切换会涉及到用户态到内核态的切换原因所在

用户进程

指不需要内核支持而在用户程序中实现的线程,其不依赖于操作系统核心,应用进程利用线程库提供创建、同步、调度和管理线程的函数来控制用户线程。另外,用户线程是由应用进程利用线程库创建和管理,不依赖于操作系统核心。不需要用户态/核心态切换,速度快。操作系统内核不知道多线程的存在,因此一个线程阻塞将使得整个进程(包括它的所有线程)阻塞。由于这里的处理器时间片分配是以进程为基本单位,所以每个线程执行的时间相对减少。

内核线程

线程的所有管理操作都是由操作系统内核完成的。内核保存线程的状态和上下文信息,当一个线程执行了引起阻塞的系统调用时,内核可以调度该进程的其他线程执行。在多处理器系统上,内核可以分派属于同一进程的多个线程在多个处理器上运行,提高进程执行的并行度。由于需要内核完成线程的创建、调度和管理,所以和用户级线程相比这些操作要慢得多,但是仍然比进程的创建和管理操作要快。大多数市场上的操作系统,如Windows,Linux等都支持内核级线程。

内核与用户的区别原理

先看图,看着下图结合文字理解

image.png

Java线程与系统内核线程关系

image.png

jvm中创建线程有两种方式

  • new java.lang.Thread().start()

  • 使用JNI将一个native thread attach到JVM中(这个比较抽象一些)

先说第一种,Thread().start()

//实现Runnable
public class RunnableThread implements Runnable {
    @Override
    public void run() {
        System.out.println('用实现Runnable接口实现线程');
    }
}



//继承Thread类
public class ExtendsThread extends Thread {
    @Override
    public void run() {
        System.out.println('用Thread类实现线程');
    }
}



//还有我们熟悉的线程池创建线程
static class DefaultThreadFactory implements ThreadFactory {
 
    DefaultThreadFactory() {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() :
            Thread.currentThread().getThreadGroup();
        namePrefix = "pool-" +
            poolNumber.getAndIncrement() +
            "-thread-";
    }
 

    public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r,
                    namePrefix + threadNumber.getAndIncrement(),
0);

        if (t.isDaemon())
            t.setDaemon(false);
        if (t.getPriority() != Thread.NORM_PRIORITY)
            t.setPriority(Thread.NORM_PRIORITY);
        return t;
    }
}

上述只是我列举了其中几种,但是创建线程只有一种方式(归根到底都是Thread().start(),大家可以看看我以前写过的一篇

为何说只有 1 种实现线程的方法

下面说第二种

使用JNI将一个native thread attach到JVM中

针对 new java.lang.Thread().start()这种方式,只有调用start()方法的时候,才会真正的在JVM中去创建线程,主要的生命周期步骤有:

1、 创建对应的JavaThread的instance

2、 创建对应的OSThread的instance

3、创建实际的底层操作系统的native thread

4、准备相应的JVM状态,比如ThreadLocal存储空间分配等

5、底层的native thread开始运行,调用java.lang.Thread生成的Object的run()方法

6、当java.lang.Thread生成的Object的run()方法执行完毕返回后,或者抛出异常终止后,终止native thread

7、释放JVM相关的thread的资源,清除对应的JavaThread和OSThread

针对JNI将一个native thread attach到JVM中,主要的步骤有:

  •  通过JNI call AttachCurrentThread申请连接到执行的JVM实例

  • JVM创建相应的JavaThread和OSThread对象

  • 创建相应的java.lang.Thread的对象

  • 一旦java.lang.Thread的Object创建之后,JNI就可以调用Java代码了

  • 当通过JNI call DetachCurrentThread之后,JNI就从JVM实例中断开连接

  • JVM清除相应的JavaThread, OSThread, java.lang.Thread对象

下面看幅完整的生命周期图:

image.png

OK。今天的学习就到这里,线程是我们最基础的部分,我们一定要深入理解,后面我会推出jvm专题

总结

感谢你的阅读,如果你感觉学到了东西,麻烦您点赞,关注。也欢迎有问题我们下面评论交流

加油! 我们下期再见!