synchronized 和 volatile,是最基础的两个锁!
volatile是轻量级锁,它在多核处理器开发中保证了共享变量的可见性。即当一个线程修改一个共享变量时,其他线程能够读到这个修改的值。它比syncronized使用和成本更低。
要说volatile的实现原理,就要先看一下cpu基本概念。java语言规范第3版中对volatile的定义如下:
volatile会添加一条lock# 前缀的指令在汇编前,使数据立即从缓冲区刷到主内存,同时通过缓存一致性协议,将其他缓存了该内存地址的缓存失效。从而达到内存可见的目的。
synchronized是重量级锁,它是存放在java对象头里,如图:
在运行期间,mark word里的数据会随锁标志位变化而变化。
syncronized 实现锁方法实现为,在进入前加入monitorenter和在退出时monitorexit,每个对象都有一个monitor!
锁分偏向锁,轻量级锁和重量级锁,偏向锁的前提是线程切换的可能性比较少,在对象头中保存线程偏向标识,在释放锁只需判定该偏向锁是否为需要的即可,如果有竞争则进行锁升级,到轻量级锁,再到重量级锁。锁只能正向升级,不能反向降级;
cpu中实现原子操作原理为,首先处理器能自动保证基本的内存操作的原子性,从系统内存读取或写入一个字节是原子的,即当一个处理器在读取一个字节时,其他处理器是不能访问这个字节的内存地址的。另外比如某处理器能自动保证单处理器对同一个缓存行里进行16/32/64位的操作是原子的,另外的复杂操作如跨总线宽度跨多缓存行跨页表的访问则是不能保证原子的。但是,处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性,从而使锁一类的操作得到了保障。
其中,总线锁定开销比较大。实现为使用处理器提供的一个lock # 信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,从而使该处理器以独占方式使用内存。
缓存锁定机制则是为了减轻总线锁带来的巨大开销而出来的,即如果某个操作被处理器缓存在处理器的缓存行里,并且声明了lock #操作,就可以只修改内部的内存地址,并允许它的缓存一致性来保证原子性,缓存一致性会阻止同时修改两个以上处理器缓存的内存区域数据,当其他处理器回写锁定的缓存行时,会使缓存行失效,从而保证操作的原子性。但是两种情况不能用缓存锁定,一操作的数据不能被缓存在处理器内部时,或者操作数据跨多个缓存行时,使用总线锁定;二处理器不支持缓存锁定;
jmm内存模型为,主内存和工作存,每个线程直接与工作内存交互,在不确定的时间点将数据刷入主内存,只有刷入主内存的数据对其他线程才是可见的。
锁机制保证了只有获得锁的线程才能操作锁定的内存区域,有偏向锁,轻量级锁和互斥锁,但除了偏向锁外,其他两个都是用cas来实现,即获取锁时用cas保证获取,退出同步块时使用循环cas释放锁。由此可见,cas在java锁实现中的重要地位。
但是我们也要明白 cas 也有自己的弊端,如ABA问题,循环时间长时开销很大;
基于cas,java中实现多个常用的原子操作,一般以 AtomicXXX 命名,在需要强一致性多线程操作场景,可以派上用场。