Java 并发编程线程池机制 ( 测试线程开销 | 启动线程分析 | 用户态 | 内核态 | 用户线程 | 内核线程 | 轻量级进程 )

Posted 韩曙亮

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 并发编程线程池机制 ( 测试线程开销 | 启动线程分析 | 用户态 | 内核态 | 用户线程 | 内核线程 | 轻量级进程 )相关的知识,希望对你有一定的参考价值。





一、测试线程开销



线程池是线程的缓存 , 在 Java 高并发场景中 , 所有的异步操作 , 都可以使用线程池 ;

使用线程池时 , 不建议用在 " 执行耗时较长的操作 " 的业务场景中 ;

线程池机制 最重要的功能就是 复用线程 ; 线程的创建 , 销毁 , 都是要消耗资源的 , 如果频繁创建销毁线程 , 会消耗很多资源 ;


1、正常测试


下面开始测试一下线程创建的开销 :

在主线程中 , 启动 10 10 10 万个线程 , 每个线程中累加 count 变量 ;

public class Main {

    /**
     * 线程中对该值进行累加操作
     */
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        // 记录程序开始执行时间
        long startTime = System.currentTimeMillis();

        // 创建 10 万个线程, 开启线程后, 向集合中添加一个元素
        for (int i = 0; i < 100000; i ++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    count ++;
                }
            });

            // 启动线程
            thread.start();
            // 等待线程执行完成
            thread.join();
        }

        // 记录程序执行结束时间
        long endTime = System.currentTimeMillis();
        // 打印消耗的时间
        System.out.println("耗时 : " + ( endTime - startTime ) + " ms , 最终 count = " + count);
    }
}

执行结果 : 10 10 10 万个线程执行完毕消耗 10.992 10.992 10.992 秒 ;


2、不创建线程


注释掉线程相关代码 :

public class Main {

    /**
     * 线程中对该值进行累加操作
     */
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        // 记录程序开始执行时间
        long startTime = System.currentTimeMillis();

        // 创建 10 万个线程, 开启线程后, 向集合中添加一个元素
        for (int i = 0; i < 100000; i ++) {
            /*Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    count ++;
                }
            });

            // 启动线程
            thread.start();
            // 等待线程执行完成
            thread.join();*/
        }

        // 记录程序执行结束时间
        long endTime = System.currentTimeMillis();
        // 打印消耗的时间
        System.out.println("耗时 : " + ( endTime - startTime ) + " ms , 最终 count = " + count);
    }
}

执行结果 : 1 1 1 ms 执行完毕 ; 说明耗时操作是在 for 循环中 ;


3、只创建不启动线程


注释掉线程启动代码 :

public class Main {

    /**
     * 线程中对该值进行累加操作
     */
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        // 记录程序开始执行时间
        long startTime = System.currentTimeMillis();

        // 创建 10 万个线程, 开启线程后, 向集合中添加一个元素
        for (int i = 0; i < 100000; i ++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    count ++;
                }
            });

            /*// 启动线程
            thread.start();
            // 等待线程执行完成
            thread.join();*/
        }

        // 记录程序执行结束时间
        long endTime = System.currentTimeMillis();
        // 打印消耗的时间
        System.out.println("耗时 : " + ( endTime - startTime ) + " ms , 最终 count = " + count);
    }
}

执行结果 : 耗时 79 79 79 ms , 也很快 , 大部分时间都在 启动 与 等待线程执行完毕消耗 ;


4、只启动不等待执行完成


注释掉等待线程执行完成代码 :

public class Main {

    /**
     * 线程中对该值进行累加操作
     */
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        // 记录程序开始执行时间
        long startTime = System.currentTimeMillis();

        // 创建 10 万个线程, 开启线程后, 向集合中添加一个元素
        for (int i = 0; i < 100000; i ++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    count ++;
                }
            });

            // 启动线程
            thread.start();
            // 等待线程执行完成
            //thread.join();
        }

        // 记录程序执行结束时间
        long endTime = System.currentTimeMillis();
        // 打印消耗的时间
        System.out.println("耗时 : " + ( endTime - startTime ) + " ms , 最终 count = " + count);
    }
}

执行结果 : 耗时 3.866 3.866 3.866 秒 ;





二、分析测试结果




1、启动线程分析


在上述测试中 , 如果只是创建 10 10 10 万个 Thread 对象 , 这些在 Java 中就是普通的对象 ;

但是如果调用了 Thread 对象的 start() 方法 , 就要涉及到系统的线程切换 , 这个操作非常耗时 ;


操作系统的空间 , 分为 用户空间内核空间 ;

用户空间中 , 有多个进程 , 每个进程有多个线程 , 每个进程都有一个 线程表 , 用于保存该进程中的线程 ;

JVM 创建的线程是 内核线程 ;

执行 main 函数时 , 处于 用户态 , 一旦调用了 start() 方法启动了线程 , 此时就进入了 内核态 , 该状态切换消耗巨大 ;


2、用户线程与内核线程


系统的线程分为 用户线程 和 内核线程 ;

用户线程 : 用户线程是 用户程序实现的线程 , 并负责管理线程的 创建 , 执行 , 调度 , 同步 ;

  • 线程阻塞时 , 进程也会阻塞 ;

( Java 没有用到用户线程 )


内核线程 : 内核线程是 由内核管理的线程 , 其内部保存了线程的状态信息 , 上下文信息 , 如果频繁的切换线程 , 需要反复处理状态信息 , 上下文信息 , 会浪费很多资源 ;

  • 线程阻塞时 , 进程不会阻塞 ;
  • 内核线程效率比用户线程低 , 比进程高 ;


3、轻量级进程


轻量级进程 : 在我们写的程序中 , 虽然使用了内核线程 , 但 没有直接使用 , 而是 通过内核线程的高级接口使用内核线程 , 这个高级接口就是 " 轻量级进程 " , Java 程序中的 Thread 就是轻量级进程 , 每个 轻量级进程 都对应一个 内核线程 ;


4、验证 Java 线程类型


在任务管理器中可以查看线程数 :

执行下面的程序 : 创建了 10000 10000 10000 个线程

public class Test {
    public static void main(String[] args) {
        for (int i = 0; i < 10000; i ++){
            new Thread(()->{
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

创建 10000 10000 10000 线程后 , 发现线程数增加了 10000 10000 10000 ;

由此可见 , Java 虚拟机创建的线程是内核线程 ;


Java 虚拟机创建线程 , 依赖于系统内核 , 内核空间的内核线程 与 用户空间的 Java 线程 是一一对应的关系 ;

以上是关于Java 并发编程线程池机制 ( 测试线程开销 | 启动线程分析 | 用户态 | 内核态 | 用户线程 | 内核线程 | 轻量级进程 )的主要内容,如果未能解决你的问题,请参考以下文章

Java 并发Executor框架机制与线程池配置使用

Java并发——线程池Executor框架

Java并发——线程池Executor框架

多线程--Executor线程池框架

Java并发编程- 线程调度 - 线程池

Java 并发编程线程池机制 ( 线程池阻塞队列 | 线程池拒绝策略 | 使用 ThreadPoolExecutor 自定义线程池参数 )