-
为什么要进行多任务处理?
1. 计算机运算能力强大。
2. 计算机的运算速度与它的存储和通信子系统的速度差距太大,大量时间花在磁盘I/O、网络通信或者数据库访问上,必须要激发计算机的运算能力,减少造成性能浪费。 -
高速缓存
- 为什么要使用高速缓存?:计算机的存储设备和处理器的运算速度差距太大,导致计算机需要添加高速缓存来进行一个缓冲。
- 高速缓存的作用:读写速度接近处理器的运算速度,能够将运算所需要的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存中,这样处理器就无需等待缓慢的内存读写。
-
缓存一致性问题
1. 原因: 在多路处理器系统,每个处理器都有自己的高速缓存,而这些高速缓存又共享了一个主内存,当多个处理器的运算任务都涉及到同一块主内存区域时,可能会导致各自的缓存数据不一致。
2. 解决方法:各个处理器访问缓存时都遵循一些协议,在读写时根据协议来进行操作,比如MSI,MESI,MOSI等
-
如何充分利用处理器的运算能力?
1. 高速缓存
2. 处理器对输入代码进行乱序执行优化,打乱执行顺序,计算之后将执行结果重组,不保证代码执行顺序和输入代码一致,但保证结果与顺序执行结果一致。
Java内存模型
-
Java内存模型的主要目的:所有的变量都存储在主内存。定义程序中各种变量的访问规则,即关注虚拟机中把变量值存储到内存和从内存取出变量值这样的底层细节。
【补充!!】:这个变量值包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,自然就不会存在竞争问题。
6. 所有的变量都存储在主内存,但每个线程都有自己的工作内存,线程的工作内存中保存了被该线程使用的变量的主内存副本,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的数据。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
【补充!!】:如果局部变量是一个reference类型,它引用的对象在Java堆中可被各个线程共享,但是reference本身在Java栈的局部变量表中是线程私有的。
【补充!!】:虽然是将主内存中的变量赋值到工作内存中,但不是整个对象进行复制,而是复制比如对象的引用、对象中某个线程访问到的字段等。
-
主内存和工作内存的关系:二者没什么关系,从变量、主内存、工作内存的定义来看,主内存主要对应于Java堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域。
-
主内存和工作内存的8个交互协议
8个操作:
如果要把变量从主内存中拷贝回工作内存,需要顺序执行read和load,如果要把变量从工作内存同步回主内存,需要顺序执行store和write。
8个基本操作的规则:
1. 不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者工作内存发起回写了但主内存不接受的情况出现。
2. 不允许一个线程丢弃它最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
3. 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存 中。
4. 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或 assign)的变量,换句话说就是对一个变量实施use、store操作之前,必须先执行assign和load操作。
5. 一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。即同一时刻一个对象只能被一个线程上锁,但可重入,最终要释放相同次数的锁。
6. 如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作以初始化变量的值。即保证每次加锁时得到的变量值都是最新的。
7. 如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个 被其他线程锁定的变量
8. 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。 -
Volatile
当一个变量被定义成volatile后,它具有两个特征:
1. 可见性。保证这个变量对所有线程的可见性,即当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的,它会使其他线程中这个变量的缓存值立刻失效。即将本处理器的缓存写入了内存,该写入动作也会引起 别的处理器或者别的内核无效化(Invalidate)其缓存。
【扩展】:synchronized和final也能保证可见性,synchronized能保证在同一时刻只能有一个线程 执行同步代码块的内容,不会收到其他线程干扰。final关键字:被final修饰的字段在构造器中一旦被初始化完 成,并且构造器没有把“this”的引用传递出去,那么在其他线程中就能看见final字段的值
即final修饰的字段不可变,其他线程都可见。
2. 禁止指令重排序。普通变量仅仅会保证方法的执行过程中所有依赖赋值的地方都能正确,但不能保证赋值操作的顺序和程序代码的顺序一致。
可以看出,volatile定义的变量在汇编代码中会多出一个lock
前缀,此前缀的lock整条指令相当于一个内存屏障,指重排序时不能把后面的指令重排序到内存屏障之前的位置。lock addl$0x0,(%esp)
指令把修改同步到内存时,意味着所有之前的操作都已经执行完成,形成内存屏障的效果。
【扩展】:synchronized也能保证有序性,因其允许变量在同一时刻只允许一个线程对其进行lock操作
,而单线程对变量操作的结果与顺序执行的结果是一致的。
3. volatile的问题:
1. 不能保证原子性,对于i++ 问题,只能保证每次获取到的i都是当前的最新值,不能保证后面的+1操作不会被其他线程干扰。
2. 写操作的性能比普通变量慢一些,因为要插入内存屏障指令来保证处理器不会发生乱序执行
【扩展:如何保证原子性】 :1.使用原子类。2.使用CAS机制进行变量的更新。3.使用synchronized。4.lock。
Java与线程
-
实现线程的三种方式:
1. 使用内核线程实现(1:1实现)。
2. 使用用户线程实现(1:N实现)。
3. 使用用户线程和轻量级进程混合实现(N:M)实现 -
内核线程实现
使用内核线程实现的方式也被称为1:1实现。内核线程就是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。
程序在使用内核线程时,一般都是使用进程(内核线程的高级接口)。由于内核线程的支持,每个轻量级进程都成为一个独立的调度单元,即使其中某一个轻量级进程 在系统调用中被阻塞了,也不会影响整个进程继续工作。
内核线程的局限:
1. 由于是基于内核线程实现的,所以各种线程操作,如创建、析构及同步,都需要进行系统调用。而系统调用的代价相对较高,需要在用户态和内核态中来回切换。
【为什么内核线程调度切换成本高?】:内核线程调度成本主要来自于用户态和核心态的状态转换,这两种转换状态开销来自响应中断、保护和恢复执行现场的成本。
2. 其次,每个轻量级进程都需要有一个内核线程的支持,因此轻量级进程要消耗一定的内核资源(如内核线程的栈 空间),因此一个系统支持轻量级进程的数量是有限的。 -
用户线程实现
使用用户线程实现的方式被称为1:N实现。广义上来讲,一个线程只要不是内核线程,都可以认为是用户线程的一种。狭义上的用户线程指的是完全建立在用户空间的线程库上,系统内核不能感知到用户线程的存在及如何实现的。用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。
用户线程的优点:不需要系统内核支援,切换时不需要进行系统调用,减少资源消耗。可以支持大规模的用户线程并发
用户线程的缺点:所有的线程操作都 需要由用户程序自己去处理。 -
混合实现 N:M
将内核线程与用户线程一起使用的实现方式,被称为N:M实现。在这种混合实现下,既存在用户线程,也存在轻量级进程。用户线程还是完全建立在用户空间中,因此用户线程的创建、切换、析构等操作依然廉价,并且可以支持大规模的用户线程并发。而操作系统支持的轻量级进程则作为用户线程和内核线程之间的桥梁, 这样可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级进程来完成,这大大降低了整个进程被完全阻塞的风险。
-
Java线程的实现
1. 以HotSpot为例,它的每一个Java线程都是直接映射到一个操作系统原生线程来实现的,而且中间 没有额外的间接结构,所以HotSpot自己是不会去干涉线程调度的,全权交给底下的操作系统去处理。
2. 也有默认使用1:N由用户线程实现的线程模型,所有线程都映射到一个内核线程,但当Java线程执行一个阻塞调度时就会单独开一个内核线程
3. 也有使用N:M实现的 -
Java线程调度
线程调度是指系统为线程分配处理器使用权的过程,调度主要方式有两种,分别是协同式线程调度和抢占式线程调度。- 协同式线程调度:线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,要主动通知系统切换到另外一个线程上去。
- 协同式线程调度的优点:实现简单,而且由于线程要把自己的事情干完后才会进行线程切换,切换操作对线程自己是可知的,所以一般没有什么 线程同步的问题。
- 协同式线程调度的缺点:线程执行时间不可控制,甚至如果一个线程的代码编写有问题,一直不告知系统进行线程切换,那么程序就会一直阻塞在 那里。
- 抢占式线程调度:那么每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定。
- 抢占式线程调度的优点:线程的执行时间是系统可控 的,也不会有一个线程导致整个进程甚至整个系统阻塞的问题。
- 抢占式线程调度的缺点:是如果想要主动获取执行时间,线程本身是没有什么办法的。比如yield()方法让出执行时间后必须等后续通知。
-
上下文切换的过程:当中断发生,从线程A切换到线程B去执 行之前,操作系统首先要把线程A的上下文数据妥善保管好,然后把寄存器、内存分页等恢复到线程B 挂起时候的状态,这样线程B被重新激活后才能仿佛从来没有被挂起过。
-
什么是“上下文”:以程序员的角度来看,是 方法调用过程中的各种局部的变量与资源;以线程的角度来看,是方法的调用栈中存储的各类信息; 而以操作系统和硬件的角度来看,则是存储在内存、缓存和寄存器中的一个个具体数值。
-
协程:协程是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行),所以能减少线程切换。
-
纤程 :纤程是有栈协程的一个特例。一段使用纤程并发的代码会被分为两部分——执行过程(Continuation)和调度器(Scheduler)。
执行过程主要用于维护执行现场,保护、恢复上下文状态。
调度器则负责编排所有要执行的代码的顺序。
将调度程序与执行过程分离的好处是,用户可以选择自行控制其中的一个或者多个,而且Java中现有的调度器也可以被直接重用。