文章目录
volatile
内存可见性
当变量被volatile
关键字修饰后,CPU将越过寄存器向主内存中直接请求。可理解为使用同一监视器对单个读写操作同步
在这里我们可以说基于volatile读写在多线程中是安全的,当时基于volatile的运算是不安全的。
原因在于JMM允许多个线程同时计算volatile变量,但运算操作却不是原子的
底层实现
如何volatile变量对不同线程的可见性
- 缓存一致性协议
在CPU写入数据时,如操作的是volatile变量且其他CPU中存在该变量的副本,则发出通知告知其他CPU将该变量缓存行设置为无效。当其他CPU读取时向主内存重新读取 - 嗅探
每个处理器嗅探总线上数据,检查缓存是否过期,当发现缓存行对应的内存地址被修改,置缓存行无效
总线风暴:当大量使用volatile时,将导致大量主内存嗅探及无效CAS,从而使总线带宽达到峰值
保证有序性
在单线程下,由于as-if-serial语义的存在,无论对指令集如何排序,其执行结果不变,具体表现为:不对 存在数据依赖关系的操作重排
但多线程切换的随意性,使得我们从外部看线程实际上无序的,这样的弱有序性不能满足并发要求
volatile通过防止指令重排来保证有序性,底层使用内存屏障,部分指令先行而部分指令后行
是否重排 | second operate | ||
first operate | 普通读写 | volatile读 | volatile写 |
普通读写 | NO | ||
volatile读 | NO | NO | NO |
volatile写 | NO | NO |
可见
- 当第二个操作为volatile写时,无论第一个操作时什么都不能重排
确保volatile写之后的操作不会被编译器重排到volatile写之前 - 当第一个操作为volatile读时,无论第二个操作是什么都不能重排
确保volatile读之后的操作不会被编译器重排到volatile读之后 - 对于volatile变量,write happens-before read
保证64位数据的读写原子性
synchronized
synchronized关键字使代码同步于某个对象上。所有同步在一个对象的同步块只能被一个线程进入并执行,其他阻塞。以下为sync的四种常用形式
(如果不理解什么是监视器可以跳到sync机制实现的说明)
-
实例方法同步
public synchronized void func(){ System.out.println("sync in instance method"); }
同步在 拥有该方法的对象上 ,也就是this指针上。所以只允许一个线程执行一个实例方法
-
静态方法同步
public static synchronized void func(){ System.out.println("sync in instance static method"); }
同步在该方法所在的 类对象 ,即XX.class。允许有多个线程同时操作该类不同实例,但仅允许一个线程执行该类的静态方法
-
实例方法中同步代码块
Object monitor = new Object(); public void func(){ synchronized (monitor){ System.out.println("sync in instance method code"); } }
同步在 监视器 对象上,就是对sync关键字传入的对象。仅允许获得监视器对象的线程执行,每个监视器仅容许一个线程获取
当采用this作为监视器,近似等效于sync修饰方法名public void func(){ synchronized (this){ System.out.println("sync in instance method code"); } }
-
静态方法中同步块
static Object monitor = new Object(); public static void func(){ synchronized (monitor){ System.out.println("sync in instance method code"); } }
仍然同步在 监视器 对象上,但是由于静态对象初始化时间的特殊性,要注意监视器对象的选取。
当采用类对象作为监视器时,近似等效于sync修饰方法名前提是你把代码块都包起来public static void func(){ synchronized (T1.class){ System.out.println("sync in instance method code"); } }
注意:
- 类对象与锁对象并不冲突。根本在于这是两个不同的对象,也就是两个不同的监视器,我们可以在执行同步实例方法的同时,执行同步静态方法
- sync修饰方法时,在.class文件中设置方法的ACC_SYNCHRONIZED访问标识;对于代码块的同步则是依赖于
monitorenter/monitorexit
指令,当执行到monitorenter
指令时尝试获取监视器所有权
sync同步机制实现
sync同步借助 monitor机制以及wait/notify共同实现
考虑如下场景:现在有台电脑在一个房间中,一群人排队上网,但是这个房间只允许一个人进入,为了防止别人进去,先进去的从里面把门锁上,这样外面的人想进去的时候发现门打不开了,便只能在外面等,时不时来试试这门能不能开。但是时间一长总不能干等吧,于是有的人就开始征战王者峡谷了,不管这门能不能进了。当里面的人玩完了,可能出来的时候吼一嗓子告诉所有人我下机了,然后所有人就去抢房间,谁先抢到是谁的,抢不到老倒霉蛋了;也有可能他出来看见打王者的太可怜,随便挑了一个和他说:无内鬼,门没锁,然后这货就去了。
现在把人换成线程,房间变为共享内存,就是整个sync的同步机制了。
什么是monitor
在上述场景中,monitor就是人进去反手锁上的那扇门。在Java中通过对象头信息实现。
当线程想要获取锁时,会在对象头里的Mark Word里查找锁状态,若无锁则CAS添加自己的信息给锁;如果锁对象已被其他线程占用,那就只能一边凉快去了。
关于对象头更详细的说明,如为什么在sync中调用hashCode()会导致直接生成轻量级锁:null
锁升级
在第一次进入同步之后,首先生成偏向锁,CAS修改对象头里的锁标志位。偏向锁偏向于第一个获得它的线程A,执行完毕之后不主动释放,monitor上持有锁的线程不改变仍为A。这样如果线程A继续对该同步块进行操作,则无需获取monitor。
如果有其他线程B也需要对该同步块进行操作,出现锁竞争时,锁升级为轻量级锁 。别看说的花里胡哨,实际就一自旋锁 此时两线程循环重试执行条件。当自旋次数达到最大次数后,继续自旋影响程序性能,于是再次升级锁为重量级锁。
此时调用wait
方法将超出最大值的自旋线程挂起,放弃对CPU使用权的竞争,减少性能损耗。直到待获取锁对象被释放,调用notify
唤醒线程,重新竞争锁
重入性
允许同一个线程多次获取同一把锁
final
推荐阅读:你以为你真的了解final吗?
final域写重排
JMM禁止final域写重排到构造器之外。
保证在对象引用为任意线程可见前被正确初始化(无论哪个线程在何时,该对象引用都连到了一个初始完毕的内存空间) 如果构造对象不从构造器逸出
实现:编译器在final写之后插入storestoer屏障
final读重排
在初次读对象引用和该对象包含的finial域,JMM禁止重排
读对象final域前,一定先读包含final域对象引用
实现:在读final前加入loadload屏障
final域为引用对象
对final修饰对象成员写入 happens-before 构造对象引用的赋予。
意思就是最后交给你一个从引用连到内存的整体,而不是缺斤少两的奇葩。。
Lock接口
不同于volatile和sync这样的关键字,Java提供了可自定的同步控制组件:Lock接口
Lock lock = new ReentrantLock();
lock.lock();
try{
System.out.println("Using lock!");
}finally{
lock.unlock();
}
需要注意的是synchronized同步块执行完成或者遇到异常是锁会自动释放,而lock必须调用unlock()方法释放锁,因此在finally块中释放锁。
使用lock的优点是可以自定义lock。我们可以继承AQS并定义若干同步状态的获取和释放,从而实现自己的lock实现。
文章参考:
1.通俗易懂 悲观锁、乐观锁、可重入锁、自旋锁、偏向锁、轻量/重量级锁、读写锁、各种锁及其Java实现! - Pickle Pee的文章 - 知乎
2. CL0610 /Java-concurrency