多线程的出现
计算机的运算能力太强大了,它的运算速度和它的通信子系统的速度差距太大,大部分时间都花费在磁盘IO、网络通信或者访问数据库上。为了避免这部分浪费,将IO等待的时间利用起来,就必须采用多线程的方案。
硬件的效率与一致性
由于计算机的存储设备与处理器的运算速度有着几个数量级的差距,所以现代计算机系统都不得不加入一层或多层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲:将运算需要使用的数据复制到缓存中,让运算能快速地进行,当运算结束后能再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。
这种基于高速缓存的存储交互很好地解决了处理器与内存速度之间的矛盾,但是也为计算机系统带来更高的复杂度,它引入了一个新的问题:缓存一致性(Cache Coherence)。在多路处理器系统中,每个处理器都有自己的高速缓存,而他们又共享同一主内存,这种系统称为共享内存多核系统。当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致。所以为避免这些情况,各个处理器需要尊顺一些缓存一致性协议(MSI、MESI)
Java内存模型可以理解为:在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象
除了增加高速缓存外,为了使处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执行(Out-Of-Order Execution)优化,Java虚拟机的即时编译器中也有指令重排序(Instruction Reorder)优化。
Java内存模型
Java内存模型的主要目的是定义程序中各种变量的访问规则,即关注在虚拟机中把变量值存储到内存和从内存中取出变量值这样的底层细节。具体包括实例字段、静态字段、和构成数组对象的元素(共享变量),而不包含局部变量和方法参数。
Java内存模型规定了所有的变量都存储在主内存(Main Memory)中,同时每条线程还可以有自己的工作内存,线程的工作内存中保存了被该线程使用的变量的主内存副本
- 线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的数据
- 不同的线程之间也无法直接访问对方的工作内存中的变量
- 线程间变量值的传递均需要通过主内存来完成
volatile
关键字volatile是Java虚拟机提供的最轻量级的同步机制。当一个变量被定义为volatile之后,它将具备两项特性:1.保证此变量对所有线程的可见性(修改后其他线程可知),2.禁止指令重排序优化
- 可见性
其他线程可见。在线程A修改了缓存中的值时,会立刻通过总线推送给主内存。可见性体现在总线这一概念上,其他的CPU一直在嗅探总线的数据流通,通过嗅探在缓存一致性协议MESI的保障下,能够嗅探到数据的修改,如果自己的缓存行中有这条数据,将这条数据置为不可用。下次需要读或写这条数据的时候直接从主内存拉取到缓存行中。嗅探是通过汇编语言实现的,如果对一个volatile变量的修改推送到总线的时候,汇编指令会在里面加一个lock指令,lock指令有两层含义:1.将信息的修改推送到主内存,2.其他的CPU会嗅探带lock指令的汇编指令,然后置其他缓存行为不可用。 - 有序性(禁止指令重排序)
禁止指令重排序发生在编译阶段,方法表中的属性表的Code属性当中,存放字节码。volatile是串行化从内存读取某个变量(对象地址),实际上是变量锁
– 对于volatile写前会加入store-store屏障,写写不能进行指令重排序
– 对于volatile写后会加入store-load屏障,写读不能进行指令重排序
– 对于volatile读前会加入load-load屏障,读读不能进行指令重排序
– 对于volatile读后会加入load-store屏障,读写不能进行指令重排序(全能型屏障,开销比较大) - 原子性
在特殊情况下可以保障原子性,单条字节码可以保证原子性。
补充:单例模式中的双重检查锁
在第一次创建的过程中,线程A需要new一个对象出来,开辟内存空间,初始化,指向内存空间这三步骤。加上volatile后,第二步和第三步仍有可能存在重排序现象,在jvm中new对象的时候只有一个指令码new,属于是对内存中的写操作,需要在写前加入store-store屏障,写后加一个sotre-load屏障,避免外层线程B对其的空判断
synchronized
作用在代码块上 —— 锁的是特定的obj
作用在普通方法上 —— 锁的是当前对象
作用在静态方法上 —— 锁的是当前的Class
Java线程的实现
线程是比进程更轻量级的调度执行单位,线程的引入可以把一个进程的资源分配和执行调度分开,各个线程即可以共享进程资源(内存地址/IO等)又可以独立调度。
实现线程主要有三种方式:
- 使用内核线程实现(1:1实现)
- 使用用户线程实现(1:N实现)
- 使用用户线程加轻量级进程混合实现(N:M)
Java是采用内核线程实现的。内核线程就是直接由操作系统内核(Kernel)支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器对线程进行调度。
程序一般不会直接使用内核线程,而是使用内核线程的一种高级接口——轻量级进程,轻量级进程就是我们通常意义上所讲的线程,由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程才能有轻量级进程
由于内核基于内核线程实现的,所以各种线程操作,如创建、析构、同步都需要进行系统调用,而系统调用的代价相对较高,需要在用户态和内核态中来回切换
其次每个轻量级进程都需要有一个内核线程的支持,因此轻量级进程要消耗一定的内核资源,因此一个系统的轻量级进程的数量是有限的