摘要
之前我们讲解了cpu多多级缓存模型,以及为什么需要引入cpu多级缓存模型?(为了解决cpu运算速度远高于基于I/O总线读取主内存数据速度)然后引入cpu多级缓存模型之后产生的问题?(数据缓存一致性)然后就是解决cpu缓存一致性问题的方案?(总线加锁及缓存一致性协议MESI)然后详细讲解了缓存一致性协议MESI中多线程读写主内存数据时候产生的问题;这一讲主要讲解下什么是线程以及为什么需要并发?
思维导图
我们按照以下思维逻辑导图讲解线程及并发。
内容
1、线程及进程
进程: 系统分配资源的基本单位;其就是我们运行的一个应用程序;比如:JVM进程、微信、QQ。
线程: 操作系统调度cpu的基本单位;是进程中的一个场景快照。
线程及进程关系:
1、进程是系统分配资源的基本单位;线程是调度cpu的基本单位。进程是没有调用cpu的权利的,假如我们的jvm是一个应用程序,它本身是没有调用cpu的权利的(调用cpu的权利是操作系统的功能),假如我们的jvm能够不依赖于我们的操作系统。直接可以操作cpu和显卡的话,那么这个jvm不就是一个操作系统了吗?那他还安装到操作系统干嘛呢?线程是调度cpu的基本单位,本身是不具备更多的资源,只具备自身需要的简单资源,所以说线程会共享进程里面的资源。
2、综上所叙述:线程是调度cpu的基本单位,一个进程至少包含一个线程,线程寄生在进程当中。每个线程都有一个程序计数器(记录要执行的下一条指令)、一组寄存器(保存当前线程的工作变量)。
分类:
按照线程所属空间,我们将线程分为:用户线程(User-Level Thread)、内核线程(Kernel-Level Thread);不同种类的线程工作在不同的空间里面。
我们看下如下代码:
str = “I like learning“ //用户空间
x = x + 2
file.write(str) //切换到内核空间
y=x+4 //切换到用户空间
如上图:我们的用户空间划分为两部分:用户空间、内核空间。
内核空间:系统内核运行的空间。
用户空间:用户程序运行的空间;比如JVM、PS、播放器。
我们程序运行的状态,如果运行在内核空间就是内核态,运行在用户空间的话它是用户态。为了安全性起见,两者是隔离的,即使用户空间的JVM崩溃了,内核空间不受影响的。
内核空间的话是可以执行任何命令,调用系统任何资源。用户空间的话简单运算,不能调度系统资源。内核空间会提供一个接口供用户空间发送指令。
cpu特权级别: cpu特权级别分为:ring0、ring3。
cpu特权级别:为什么我们的JVM需要依赖我们的内核才能调用我们的cpu呢?因为我们的cpu分为了两级特权。一级是ring0(最好级别,拥有cpu最高操作权限)、一级是ring3最低级别(拥有简单的操作权限,没有核心操作权限);用户空间只有cpu的ring3特权级别。内核空间只有cpu的ring0特权级别。为什么需要这样的划分?为了安全性:如果说我们的JVM在用户空间,具备cpu0的特权,那么他就能够操作我们的内核空间,这样的话通过更改内核可以植入病毒,以及控制其他应用程序。所以只有内核空间才具备操作cpu的最高特权级别。
用户线程与内核线程:
用户线程(ULT): 就是在用户空间里面的进程创建的线程。用户线程它是没有cpu的使用权限的,它是不能够直接去调度cpu(只能简单操作权限),因为我们的内核是不知道多线程的存在的。内核根本不知道多线程的用户线程存在,因为在其内部维护的是一个进程表。我们cpu处理器资源的分配时间分片的话,是以进程为基本单位的,所以线程是依托于我们的主进程去执行的。所有的线程都执行在同一条线上。这就有一个问题当我们的用户线程阻塞了,比如线程1阻塞了。整个主进程将会被阻塞。如下:
内核线程(KLT):内核线程由在内核空间创建,维护在内核空间的线程表里面(如上面图示:进程表里维护了用户空间的进程、线程表里面维护了内核空间的线程);同理,内核进程也是在内核空间创建,在内核空间维护进程表。内核级线程是操作系统去实现的。cpu会为我们的内核级线程分配时间片。所以多个线程都可以去争夺cpu资源。内核线程阻塞了的话不会影响内核进程的运行。
在java里面用的是哪种线程呢?
在java里面1.2版本之前用的是ULT;1.2之后用的是KLT内核级线程;java线程模型就是依赖于底层内核线程去维护的,两者有什么关系呢?如下:
他们是一一映射的关系。
如上:我们的jvm进程是可以创建多个线程的,本质上是jvm去创建了线程栈空间(其实没有去创建真正的线程);线程栈空间里面会有一些栈针指令; 创建真正的线程是需要通过我们的库调度器去调度我们的内核空间去创建内核线程,从而操作调度cpu;
java线程<--->内核线程。
2、并发
Java线程生命周期
Java线程生命周期的话是仅仅限制在我们的JVM里面,总共就只有6中状态:
新建: NEW
运行:RUNNABLE
等待:WAITING
阻塞:BLOCKED
终止:TERMINATED
注意,我们的阻塞之后 不是不运行了,而是进入就绪状态,等待时间片分配。
为什么用并发
1、充分利用多核CPU的计算能力。
2、方便进行业务拆分,提升应用性能。
并发产生的问题:
1、高并发场景下,导致频繁的上下文切换
2、临界区线程安全问题,容易出现死锁的,产生死锁就会造成系统功能不可用。
注意:在我们单核处理器也支持多线程代码的,只不过是cpu给每一个线程分配时间片(时分复用)来实现这种机制的。这情情况下cpu给每个线程分配的时间比较短,让我们觉得多个线程好像是同时执行的。因为这个时间片的时间只有几十毫秒,是非常的短,但是需要频繁的线程上下文切换。
还有一点要需要注意的:并发与并行区别:
并发:多个任务交替执行;比如时间片cpu的切分。
并行:才是真正意义上的多个任务同时进行。
假如我们的系统内核只有一个cpu,那么他使用多线程时候,实际上并不是并行的,他只能通过时间片的形式。去并发执行任务。真正意义上的并发只会出现在多个cpu的系统当中。
缺点:
如果在大并发条件下大量创建线程。由于我们的linux系统或者jvm他的线程数是有一个峰值的。如果超过了这个峰值的话,性能会大幅度下降。
线程上下文切换原理剖析如下:
上面所示,线程并发执行的时候一般都是通过时分复用;线程争夺cpu的时间片来执行程序的。假如线程T1在时间片A时候争夺到了cpu的权利,然后到时间片B时候,T1还没有执行完毕,但是此时时间片B已经被线程T2争夺到了,所以线程线程T1的指令、程序指针、中间数据之前是存在与cpu的寄存器跟缓存里面的,这个时候就需要通过总线将线程T1寄存器和缓存里上下文内容(那个的指令,程序指针、中间数据)保存到内核空间的主内存里面去。保存到内核栈空间的TSS任务状态段里面去。然后当我们的线程T1又争取到时间片c时候;需要从内核空间主内存里面加载上下文数据到cpu的寄存器跟缓存里面去。大量线程上下文切换造成性能下降,并且可能产生死锁。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。