美文网首页
【线程的另一种形式】

【线程的另一种形式】

作者: 蚂蚁的心脏 | 来源:发表于2022-03-10 09:36 被阅读0次

今天研究的问题:

1. Go并发忧于Java并发?
2. Go语言的并发是多线程实现的么?
3. Java并发性能如何提高?
4. 线程的分类:
4. 如何根据场景选择不同的线程实现方式?

一.线程开启方式对比

场景1:

假设我们有一个任务,平均执行时间为1秒,分别测试一下使用线程和协程并发执行100000次需要消耗多少时间。
go语言

// 线程开启
// 线程开启
func testThread(i int) {

    fmt.Println("当前值:", i )
    time.Sleep(time.Second) //延时一秒
}
func test() {
    for i := 0; i < 100000; i++ {
        go testThread(i)
    }
}

func main() {
    start := time.Now() // 获取当前时间
    test()
    elapsed := time.Since(start)
    fmt.Println("该函数执行完成耗时:", elapsed)
}
总耗时
image.png
java语言
public class TestThread01 extends Thread {
    private int i;

    public TestThread01(int i) {
        this.i = i;
    }

    public void run() {
        System.out.println(this.i);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        long startMili = System.currentTimeMillis();// 当前时间对应的毫秒数
        for (int i = 0; i < 100000; i++) {
            new TestThread01(i).start();
        }
        long endMili = System.currentTimeMillis();//结束时间
        Thread.sleep(7000);
        System.out.println("/**总耗时为:" + (endMili - startMili) + "毫秒");
    }
}
总耗时
image.png
线程池实现方式
@Configuration
@EnableAsync
public class BeanConfig {

    @Bean
    public TaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 设置核心线程数
        executor.setCorePoolSize(10);
        // 设置最大线程数
        executor.setMaxPoolSize(100000);
        // 设置队列容量
        executor.setQueueCapacity(10);
        // 设置线程活跃时间(秒)
        executor.setKeepAliveSeconds(10);
        // 设置默认线程名称
        executor.setThreadNamePrefix("hello-");
        // 设置拒绝策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // 等待所有任务结束后再关闭线程池
        executor.setWaitForTasksToCompleteOnShutdown(true);
        return executor;
    }
}

测试类:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = AppApplication.class)
public class TestThreadPool {
    @Autowired
    private Print print;

    @Test
    public void test() throws InterruptedException {
      
        long startMili = System.currentTimeMillis();// 当前时间对应的毫秒数
        for (int i = 0; i < 100000; i++) {
            print.sayHello(i);
        }
        long endMili = System.currentTimeMillis();//结束时间
        Thread.sleep(1000);
        System.out.println("/**总耗时为:" + (endMili - startMili) + "毫秒");
    }
}
总耗时:
image.png

结论:

从总体看go语言实现多线程的方式更为简洁耗时更短,go关键字轻松实现
java语言实现线程相对复杂,耗时更长
线程池实现方式需要复杂配置.
代码在执行过程中CUP占用率: Java > Java线程池 > go语言

  • 本质原因: go语言开启的不是线程--->而是协程(线程中的线程)

原因分析:

分析前需要了解:进程-线程-协程区别

进程空间分配

操作系统采用虚拟内存技术,把进程虚拟地址空间划分成用户空间和内核空间。

4GB序的进程虚拟地址空间被分成两部分:用户空间和内核空间


image.png

用户空间

用户空间按照访问属性一致的地址空间存放在一起的原则,划分成 5个不同的内存区域。访问属性指的是“可读、可写、可执行等 。

  1. 代码段代码段是用来存放可执行文件的操作指令,可执行程序在内存中的镜像。代码段需要防止在运行时被非法修改,所以只准许读取操作,它是不可写的。
  2. 数据段数据段用来存放可执行文件中已初始化全局变量,换句话说就是存放程序静态分配的变量和全局变量。
  3. BSS段BSS段包含了程序中未初始化的全局变量,在内存中 bss 段全部置零。
  4. 对 heap堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)
  5. 栈 stack栈是用户存放程序临时创建的局部变量,也就是函数中定义的变量(但不包括 static 声明的变量,static意味着在数据段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进后出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。
  • 上述几种内存区域中数据段、BSS 段、堆通常是被连续存储在内存中,在位置上是连续的,而代码段和栈往往会被独立存放。堆和栈两个区域在 i386 体系结构中栈向下扩展、堆向上扩展,相对而生。


    image.png

内核空间


image.png
线程

线程是操作操作系统能够进行运算调度的最小单位。线程被包含在进程之中,是进程中的实际运作单位,一个进程内可以包含多个线程,线程是资源调度的最小单位。

image.png
线程资源和开销

同一进程中的多条线程共享该进程中的全部系统资源,如虚拟地址空间,文件描述符文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈、寄存器环境、线程本地存储等信息。

线程创建的开销主要是线程堆栈的建立,分配内存的开销。这些开销并不大,最大的开销发生在线程上下文切换的时候。


image.png
线程分类

还记得刚开始我们讲的内核空间和用户空间概念吗?线程按照实现位置和方式的不同,也分为用户级线程(协程)和内核线程,下面一起来看下这两类线程的差异和特点。

用户级线程
image.png

实现在用户空间的线程称为用户级线程。用户线程是完全建立在用户空间的线程库,用户线程的创建、调度、同步和销毁全由用户空间的库函数完成,不需要内核的参与,因此这种线程的系统资源消耗非常低,且非常的高效。

特点
  • 用户线级线程只能参与竞争该进程的处理器资源,不能参与全局处理器资源的竞争。
  • 用户级线程切换都在用户空间进行,开销极低。
  • 用户级线程调度器在用户空间的线程库实现,内核的调度对象是进程本身,内核并不知道用户线程的存在。
缺点

如果触发了引起阻塞的系统调用的调用,会立即阻塞该线程所属的整个进程。
系统只看到进程看不到用户线程,所以只有一个处理器内核会被分配给该进程 ,也就不能发挥多久 CPU 的优势 。

内核级线程
image.png

内核线程建立和销毁都是由操作系统负责、通过系统调用完成,内核维护进程及线程的上下文信息以及线程切换。

特点
  • 内核级线级能参与全局的多核处理器资源分配,充分利用多核 CPU 优势。
  • 每个内核线程都可被内核调度,因为线程的创建、撤销和切换都是由内核管理的。
  • 一个内核线程阻塞与他同属一个进程的线程仍然能继续运行。
缺点
  • 内核级线程调度开销较大。调度内核线程的代价可能和调度进程差不多昂贵,代价要比用户级线程大很多。
  • 线程表是存放在操作系统固定的表格空间或者堆栈空间里,所以内核级线程的数量是有限的。
什么是协程

那什么是协程呢?携程 Coroutines 是一种比线程更加轻量级的微线程。类比一个进程可以拥有多个线程,一个线程也可以拥有多个协程,因此协程又称为线程的线程。


image.png
线程切换问题:

协程的调度完全由用户控制,协程拥有自己的寄存器上下文和栈,协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作用户空间栈,完全没有内核切换的开销。

线程切换
image.png
协程切换
image.png
GO语言多线程是采用哪种线程类型(GO语言并发原理)

Golang 在语言层面实现了对协程的支持,Goroutine 是协程在 Go 语言中的实现, 在 Go 语言中每一个并发的执行单元叫作一个 Goroutine ,Go 程序可以轻松创建成百上千个协程并发执行。

!!!!通过以上分析,我们可以用JAVA语言模仿GO语言的多线程实现方式--->协程?

  1. 有哪些常见的语言支持协程开发:
    python, kotlin, javascript 和go
  2. JDK是否支持协程开发?
    Java 官方目前是还没推出协程- 已加入计划
    华为的JDK支持,但并不来源
    目前可用性比较高的有 Quasar 和 ea-async 两个第三方库,都是通过 byte code Instrument,把编译后同步程序class文件修改为异步的操作。

使用JAVA实现协程

1.引入Quasar库

         <dependency>
            <groupId>co.paralleluniverse</groupId>
            <artifactId>quasar-core</artifactId>
            <version>0.7.9</version>
            <classifier>jdk8</classifier>
        </dependency>
代码实现
  public static void main(String[] args) throws Exception {
        long startMili = System.currentTimeMillis();// 当前时间对应的毫秒数
        for (int i = 0; i < 100000; i++) {
            final int count = i;
            new Fiber<>((SuspendableCallable<Integer>) () -> {
                System.out.println(count);
                Fiber.sleep(1000);
                return count;
            }).start();
        }

        long endMili = System.currentTimeMillis();//结束时间
        //阻塞等待 协程执行完毕 ----> 可采用阻塞队列
        Thread.sleep(3000);
        System.out.println("**总耗时为:" + (endMili - startMili) + "毫秒");
    }

那么场景一使用协程处理速度是多少?

image.png
场景二:

用代码生成1万个文件放入文件夹对比效率: 这里我只输出结果

  • go语言: 4.424s
    -Java语言: 4.443s
    -Java线程池: 3.208s
    -Java协程: 3.614s

结论:

换个新场景协程就没有那么明显的优势了,所以根据场景采用不通的线程开启方式
实践是检验真理的唯一标准, 遇到问题建议多采用几种方式测试
计算强 - 建议采用多核, 任务多,多协程

使用Java线程池注意事项(慎用线程池):

  1. 线程池核心设置参数
  • 核心线程数与最大线程数可以不局限于CPU的核心数(可以根据业务场景调整)
  • 队列容量设置理论可以无穷大,单不建议(意思是并发量多大的时候开启新的线程)
  • 根据业务场景设置线程拒绝策略

相关文章

网友评论

      本文标题:【线程的另一种形式】

      本文链接:https://www.haomeiwen.com/subject/zukzlrtx.html