java 底层原理_并发整理(一)— Java并发底层原理

本篇主要是底层的东西。

Java内存模型/JMM

Java并发采用的是共享内存模型。线程的通信隐式进行,整个通信过程对程序员完全透明。所以要理解其中隐式的规则,否则会引起一些内存可见性问题。

java的堆内存是可以共享的,但是栈内存是私有的。

线程A与线程B之间如要通信的话,必须要经历下面2个步骤:

首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。

然后,线程B到主内存中去读取线程A之前已更新过的共享变量。

从整体来看,这两个步骤实质上是线程 A 在向线程 B 发送消息,而且这个通信过程必须要经过主内存。JMM 通过控制主内存与每个线程的本地内存之间的交互,来为 java 程序员提供内存可见性保证。

3f5d0a19fe87

image

指令重排序

Happens-Before

这个关键字是JSR-133内存模型中用来阐述操作的内存可见性的。是JMM的核心概念。程序员要基于这个规则提供内存可见性保证来编程。

这个关系并不是说着前一个操作要在后一个操作之前,只是说前一个操作对后一个可见。

具体规则

单个线程的任意操作happens-Before后序操作

volatile写happens-Before于volatile读,原因在后面

对一个锁解锁happens-Before对一个锁加锁,原因也在后面

线程start一定happens-Before线程中任意操作

线程join成功之后一定happens-Before与返回操作

满足传递性

这个关系就是让JMM来对编译器与处理器的重排序做约束

JMM保证

因为有编译器与处理器优化的存在,所以有重排序存在的必要性。

但是JMM保证,在正确同步的情况下不改变程序的执行结果,尽可能让编译器与处理器优化。

也可以说在满足程序员定义的happens-Before规则来执行的结果与优化的结果肯定一致。

有一点要注意,JMM不保证64位的long/double变量写的原子性,因为32位处理器要执行64位数据的指令需要拆分成两个单独执行。jdk5以前的JMM,64的读/写都是分开的,jdk5以后只有写会拆分。

数据依赖性

只要两个操作访问同一个变量,并且有一个写操作,那么就说这俩是数据依赖关系。

写-读、写-写、读-写都是。

但是只对单个线程的操作和单个处理器执行的指令有效。

重排序

编译器会对指令序列做优化,并不会按照我们写的顺序执行

3f5d0a19fe87

编译器在不改变单线程中语义前提进行重排序

处理器可以改变不存在数据依赖性的语句重排序

由于读/写缓冲区,内存系统进行的重排序

1是编译器重排序,2、3属于处理器重排序。

JMM重排序规则对编译器是禁止特定类型的重排,对处理器而是采用内存屏障指令的方法。

不同的处理器都有不同的重排序规则,所以java有对应的4个内存屏障指令来禁止这些重排序。

StoreLoad最强大,就是强制让写缓冲全部刷新到内存然后再读取,大部分关键字都靠这个实现

其他三个类似LoadLoad、StoreStore、LoadStore

比如:

class Test{

int a=0;

boolean flag=false;

public void writer(){

a=1;//1

flag=true;//2

}

public void reader(){

if(flag) //3

int i=a*a;//4

}

}

//A线程先执行writer,B线程执行reader

//1和2不存在数据依赖,可以重排

//3和4不存在数据依赖,可以重排(处理器可以把指令拆分,让a*a提前读,3成立再赋值,所以3、4中的指令可以重排序)

//1和4在多线程中不考虑数据依赖,所以结果会不一样

并发原语

Volatile

会java的都知道volatile的特点

可见性:只要修饰变量,就对所有线程可见,看到其最后写入

原子性:任意单个volatile变量读/写都具有原子性

JMM怎么做到的

具体做法:

每个volatile写操作前插入一个StoreStore屏障

每个volatile写操作后插入一个StoreLoad屏障

每个volatile读操作后插入一个LoadLoad屏障

每个volatile读操作后插入一个LoadStore屏障

举例来说:StoreStore屏障的意义在于volatile写之前,所有普通写操作已经对任意处理器可见。保证这个屏障之前的写已经刷到主存。后面再加一个StoreLoad,就是防止与后面普通读重排序。volatile读类似。

效果

最终形成的可重排序效果:

第一个操作

第二个操作

是否能重排序

普通读/写

volatile读

volatile写

普通读/写

NO

volatile读

NO

NO

NO

volatile写

NO

NO

然后再看上面的例子就理解了

class VolatileTest{

int a=0;

volatile boolean flag=false;

public void writer(){

a=1;//1

flag=true;//2

}

public void reader(){

if(flag) //3

int i=a*a;//4

}

}

//A线程先执行writer,B线程执行reader

//现在2 happens-before 3,又因为单线程中1 happens-before 2同理3、4

//根据传递性现在1 happens-before 4,保证结果单一

注:我们说的volatile的原子性是指它单一的读和写,像++这样的复合操作不具有原子性

那volatile怎么保证刷回主存

在解析volatile变量写的时候,会多出一个lock汇编指令,该指令在多核处理器下会

Lock前缀指令执行期间,以前的处理器会锁住总线来,但是开销有点大,所以现在处理器会锁处理部分的内存区域,用缓存一致性来阻止两个以上的处理器缓存修改内存区域

写回结束后会被其他处理器嗅探到,然后其他处理器会把该部分置为无效,重新刷新

正是因为会锁住内存,所以有的时候在高速缓存行是64位的处理器中,我们可以将volatile变量最加到64位来提高其并发的效率。

关于如何更好使用可以看这个

Final

final用于修饰常量代表不可变,也可以修饰方法和类

所以编译器和处理器处理的时候,要保证final的赋值规范

怎么做到的

final写之后插入一个StoreStore屏障

final读前面插入一个LoadLoad屏障

当然这些都是针对大部分处理器,不同情况也会不同。

效果

对象的 final 域已经被正确初始化过了之后,才会对其他线程可见,final写也不会重排序到构造函数之外

在读一个对象的 final 域之前,一定会先读包含这个 final 域的对象的引用

锁的内容非常多,下一篇单独整理。这里写概述。

显式锁Lock

ReentrantLock都是基于volatile关键字来实现的。

通过一个volatile变量Status来控制同步的状态,使那些没有获得的线程自旋或者阻塞来实现效果。

公平锁获取的时候会读volatile,所以具有volatile语义

非公平锁获取时会先读,然后用CAS来更新,所以同时具有volatile写和读的语义

锁释放的时候都会写volatile写语义

隐式锁synchronized

synchronized之所以会叫隐式锁是因为编译器自动帮我们通过一个monitor的对象来完成。

Java中每个对象都可以作为锁,所以synchronized存在Java的对象头里。

对于synchronized代码块,JVM的实现是插入monitorenter和monitorexit指令来实现的。

方法的同步也可以用这种方式,但是JVM没有详细说明。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值