- 线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。一个进程可以有一个或多个线程,Java的多线程是通过JMM(共享内存模型)来实现的。
- 大部分的操作系统都是利用时间片轮转的抢占式调度方式,线程需要利用操作系统的调度算法在用户态和内核态来回切换,线程的切换是非常浪费资源的,所以Java的线程池在工作中才那么常用,也是面试中无法绕过的坎。
- 程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程(Lightweight Process,LWP),轻量级进程就是我们通常意义上所讲的线程,也被叫做用户线程。
- 一对一模型
一个用户线程对应一个内核线程,如果是多核的 CPU,那么线程之间是真正的并发。
缺点:
内核线程的数量有限,一对一模型使用的用户线程数量有限制。
内核线程的调度,上下文切换的开销较大(虽然没有进程上下文切换的开销大),导致用户线程的执行效率下降。
- 多对一模型
多个用户线程映射到一个内核线程上,线程间的切换由用户态的代码来进行。用户线程的建立、同步、销毁都是在用户态中完成,不需要内核的介入。因此多对一的上下文切换速度快很多,且用户线程的数量几乎没有限制。
缺点:
若一个用户线程阻塞,其他所有线程都无法执行,此时内核线程处于阻塞状态。
处理器数量的增加,不会对多对一模型的线程性能造成影响,因为所有的用户线程都映射到了一个处理器上。
- 多对多模型
结合了一对一模型和多对一模型的优点,多个用户线程映射到多个内核线程上,由线程库负责在可用的可调度实体上调度用户线程。这样线程间的上下文切换很快,因为它避免了系统调用。但是增加了系统的复杂性。
优点:
一个用户线程的阻塞不会导致所有线程的阻塞,因为此时还有别的内核线程被调度来执行;
多对多模型对用户线程的数量没有限制;
在多处理器的操作系统中,多对多模型的线程也能得到一定的性能提升,但提升的幅度不如一对一模型的高。
在Java中创建线程的方式有很多种,那么就写一个平时工作中最常见的方式,也就是上面提到的线程池。
public ExecutorService asyncExecutor() {
return new ThreadPoolExecutor(100, 200, 10,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(100), new NamedThreadFactory(ASYNC_NAME + "-"));
}
使用方法如下:
@Resource(name = "asyncExecutor")
private ExecutorService executor;
executor.execute();
Golang的协程
- 和Java不同的是协程不需要通过共享内存来通信,而是通过通信来共享内存。
- 协程也并不是Golang提出来的,协程是一种编程思想,并不局限于特定的语言。Go、Python、Kotlin都可以在语言层面上实现协程,Java 也可以通过扩展库的方式间接支持协程。协程比线程更加轻量级,可以由程序员自己管理的轻量级线程,对内核不可见。
- Golang为构建并发程序的基本代码块是:协程 (goroutine) 与通道 (channel)。他们需要语言,编译器,和 runtime 的支持。
当出现IO阻塞的时候,由协程的调度器进行调度,通过将数据流立刻yield掉(主动让出),并且记录当前栈上的数据,阻塞完后立刻再通过线程恢复栈,并把阻塞的结果放到这个线程上去跑,这样看上去好像跟写同步代码没有任何差别,这整个流程可以称为coroutine,而跑在由coroutine负责调度的线程称为Fiber。比如Golang里的 go关键字其实就是负责开启一个Fiber,让func逻辑跑在上面。
由于协程的暂停完全由程序控制,发生在用户态上;而线程的阻塞状态是由操作系统内核来进行切换,发生在内核态上。
因此,协程的开销远远小于线程的开销,也就没有了 ContextSwitch 上的开销。
假设程序中默认创建两个线程为协程使用,在主线程中创建协程ABCD…,分别存储在就绪队列中,调度器首先会分配一个工作线程A执行协程A,另外一个工作线程B执行协程B,其它创建的协程将会放在队列中进行排队等待。
当协程A调用暂停方法或被阻塞时,协程A会进入到挂起队列,调度器会调用等待队列中的其它协程抢占线程A执行。当协程A被唤醒时,它需要重新进入到就绪队列中,通过调度器抢占线程,如果抢占成功,就继续执行协程A,失败则继续等待抢占线程。
- Java 在 Linux 操作系统下使用的是用户线程+轻量级线程,一个用户线程映射到一个内核线程,线程之间的切换就涉及到了上下文切换。所以在 Java 中并不适合创建大量的线程,否则效率会很低。
- 对Java而言,操作系统会分配一块默认1M大小的栈,如果深度递归可能出现栈大小不足,但是如果一次性分配更大的内存又有可能出现浪费的现象;协程开始的时候也会分配一定大小的内存区域,一般只有2K,和线程的栈一样,协程的栈存储的也是局部变量,但是不同的是协程的栈的大小是不固定的,是可以根据需要自动调整大小的,最大甚至可以达到1G,所以灵活性非常好。
- 上面也说到Java线程需要在用户态和内核态来回切换,这样子就会很浪费资源;但是协程只是用户态。
- 协程适合于阻塞IO,对于CPU计算密集型的协程也不能很好解决。
本文只是从原理、应用场景来分析一下线程和协程的区别。



