在java多线程中用的比较多的就是volatile和synchronized了,先来看看volatile。
volatile是用来修饰变量的,volatile的作用:1、可见性
2、禁止指令重排
可见性是指当一个线程修改了变量时,其他的线程也能够看到。
指令重排是指对volatile的变量进行读写时插入内存屏障,禁止cpu以及编译期指令重排。
我们知道,cpu是有缓存的。所以对于一部分数据来讲,cpu会把他们放入cpu缓存,这样下次cpu要用这些数据的时候就不用去主存中取,这样就提高的速度。在多核情况下,多个cpu把同一个变量放入缓存,如果自顾自的操作这个变量,就会产生安全问题,所以在硬件方面,多数cpu实现了缓存一致性协议(MESI)。在缓存一致性的要求下,每个cpu都会去嗅探总线来检查自己缓存中的数据是不是过期了,但是由于cpu以及编译期会对我们的代码进行指令重排,而且cpu也不会因为以为某个变量失效而停下当前的工作取修改变量的状态,因为这将大大降低cpu的效率,也就是说硬件产商实现的MESI协议不是强一致性的,而是弱一致性的,就像是CAP理论的最终一致性。对于MESI这块讲的不是很清楚,大家可以去看看别人的博客。
在对volatile变量进行写的时候,把缓存中的数据刷新到主存,同时强制使其他cpu的缓存的该变量无效
当cpu发现自己缓存中的volatile变量无效时,会去主存中读取。
不少人去了解过MESI协议之后,会觉得MESI协议就够用了,为什么还要有volatile呢?我在这中间谈谈自己的看法
1、 MESI协议是针对多核cpu的,但是在单核cpu下还是会出现缓存不一致的问题
2、上面说了,cpu层面的MESI协议是弱一致性的,不是强一致性的,所以cpu层次提供了内存屏障以便高层实现强一致性
还有的人在了解了volatile之后会问:为什么用volatile修饰 int i,i++是不安全的呢?
这是因为i++操作分为了三步,取i,对i进行加1,写回主存。当cpuA进行到第二步的时候,cpuB开始执行,cpuB一下子执行3
步,主存中的i的值为1。这时cpuA中i的变量确实已经失效了,因为cpuA已经计算出了i+1的值了,这是一个新的值,然后cpuA回写主存,i的值仍然是1。
jdk1.6以后,优化了synchronized,引入了偏向锁和轻量级锁,锁可以升级但是不能降级。正是因为引入这两种锁,所以synchronized消耗的资源比较少
偏向锁:当一个线程访问同步代码块的时候,会在对象有以及栈帧中的锁记录里存储线程id。以后该线程进入和退出同步块时不需要进行CAS操作进行加锁和解锁,只需要简单的测试一个对象头的Mark Word里是否存储着指向当前线程的偏向锁,如果测试成功,则表示线程获得了锁,如果失败,需要测试一下Mark Word中的偏向锁标志位是否置为1,如果没有设置,使用CAS竞争锁,如果设置为1,尝试使用CAS将对象头的偏向锁指向自己。
偏向锁的撤销:偏向锁是一种等到竞争出现才释放的锁机制。偏向锁的撤销,需要等到全局安全点,即在这个时间点上没有正在执行的字节码。他首先会暂停拥有偏向锁的线程,然后检查拥有偏向锁的线程是否活着,如果线程不处于活动状态,将对象头设置成无锁状态,如果对象活着,对象头的Mark word要么重新偏向其他线程,要么恢复到无所状态。
轻量级锁:和偏向锁类似,不过在竞争的时候,没有获得锁的线程会自旋,自旋后仍然获取不到锁会导致锁升级成重量级锁。
偏向锁:加锁和解锁不需要额外的消耗,如果存在多线程竞争,锁的撤销会带来额外的消耗,只是用于一个线程访问同步代码块
轻量级锁:竞争的线程不会阻塞,但自旋会消耗cpu。适用于追求响应时间