如今java的并发编程由于大量封装好的工具类、中间件和框架的出现,使得程序员们只需要将大部分时间来关注业务逻辑即可,而里面并发的逻辑以及并发的关系已经淡出大家的视角。但是无论语言、中间件和框架如何的先进,开发人员都不能期望它们能够独立完成所有的并发事务,了解并发的内幕也是成为一个高级程序员不可缺少的课程。
一.硬件的效率与一致性
因为物理机遇到的并发问题与虚拟机中的情况有不少相似之处,因此物理机对于并发的处理方案对于虚拟机的并发处理有很大的参考意义。
由于计算机的存储设备与处理器之间的运算速度差了好几个数量级的,所以现代计算机不得不在处理器与存储设备之间增加一个高速缓存(Cache)来作为内存与处理器之间的缓冲:将运算需要的数据放入到缓存中,交由处理器处理后将数据返回给缓存中,最后通过缓存存入到内存中。
这个虽然解决了处理器与内存的计算速度差异问题,但是 新的问题出现了,那就是缓存一致性问题,从上图可以看出每个处理器都有各自的高速缓存,而他们都共享一个主内存。这样的话可能导致数据不一致。为了解决缓存数据不一致的问题,我们引入了读写时要遵循的缓存一致性协议,这些协议包括:MSI、MESI、MOSI等。
二.java内存模型
java虚拟机规范中试图将java内存模型能够适用于当前主流各个硬件和操作系统上,从而实现java程序在各个平台下都能达到一致性的内存访问效果。
2.1主内存与工作内存
java内存模型的主要目的是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出数据的细节过程。此次变量指的是线程中的共享变量。
java内存模型中也存在主内存(同物理硬件中的主内存相似),以及工作内存(同物理硬件中的高速缓存相似),也就是我之前在并发编程中提高到java线程有私有内存和共享内存。如下图:
这里所讲的主内存(共享内存)和java虚拟机中的堆、栈、方法区并不是同一个层次的,主内存主要对应于java堆中的对象实例数据部分,而工作内存则对应于虚拟机中栈的部分 区域。
2.2线程的自旋锁与自适应自旋锁
由于互斥同步对性能最大的影响是阻塞的实现,挂起和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性能带来了很大的压力。为了让线程等待时间不长,我们只需要让线程执行一个忙循环(自旋),就可以解决问题了。
因为自旋是一个忙循环,如果其他线程占用的时间很短,那这个自旋等待的效果会很好,但是如果其他线程占用时间很长,那自旋浪费的处理器资源就很大了。所以自适应自旋锁就出现了。
自适应意味着自旋的时间不在固定了,而是由前一次在同一个锁上的自旋时间及锁拥有这的状态来决定。(详细的可以去看一下我另外一篇文章,高并放编程中aqs源码解读那里)。