【线程的另一种形式】
今天研究的问题:
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.pngjava语言
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个不同的内存区域。访问属性指的是“可读、可写、可执行等 。
- 代码段代码段是用来存放可执行文件的操作指令,可执行程序在内存中的镜像。代码段需要防止在运行时被非法修改,所以只准许读取操作,它是不可写的。
- 数据段数据段用来存放可执行文件中已初始化全局变量,换句话说就是存放程序静态分配的变量和全局变量。
- BSS段BSS段包含了程序中未初始化的全局变量,在内存中 bss 段全部置零。
- 对 heap堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)
- 栈 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.pngGO语言多线程是采用哪种线程类型(GO语言并发原理)
Golang 在语言层面实现了对协程的支持,Goroutine 是协程在 Go 语言中的实现, 在 Go 语言中每一个并发的执行单元叫作一个 Goroutine ,Go 程序可以轻松创建成百上千个协程并发执行。
!!!!通过以上分析,我们可以用JAVA语言模仿GO语言的多线程实现方式--->协程?
- 有哪些常见的语言支持协程开发:
python, kotlin, javascript 和go - 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线程池注意事项(慎用线程池):
- 线程池核心设置参数
- 核心线程数与最大线程数可以不局限于CPU的核心数(可以根据业务场景调整)
- 队列容量设置理论可以无穷大,单不建议(意思是并发量多大的时候开启新的线程)
- 根据业务场景设置线程拒绝策略