2.1volatile应用
在多线程并发编程中synchronized和volatile都扮演着重要的角色,volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另一个线程能读到这个修改的值。如果volatile变量修饰使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。
2.1.1 volatile的定义与实现原理
如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值一致的。有volatile变量修饰的共享变量进行写操作的时候对应的汇编指令会带Lock前缀指令。
为什么需要“可见性”?
为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存后再进行操作,但操作完不知道何时会写到内存。由于写回到内存这个操作的时机是不确定的,所以就可能造成该共享变量已经修改(但并未写回内存),但其他缓存器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议。
volatile的两条实现原则
1. Lock前缀指令会引起处理器缓存回写到内存
2.一个处理起的缓存回写到内存会导致其他处理器的缓存无效
2.1.2 volatile的使用优化
在使用volatile变量时可以追加至64字符,但有两种情况不适用 ,1)缓存行非64字节宽的处理器,2)共享变量不会被频繁地写
不过这种追加字节的方式在JAVA7以后可能不会生效
2.2 synchronized的实现原理的应用
https://blog.youkuaiyun.com/weixin_41461319/article/details/89215510 如何使用可以看这个。
synchronized是如何实现的
每个对象都有一个monitor对象与之关联,JVM基于进入和退出Monitor对象来实现方法同步和代码同步。但两者实现细节不同。代码同步是使用monitorenter、monitorexit(这两个指令必须成对出现)指令实现。方法同步具体细节在JVM规范里并未说明,但通过这两个指令同样可以实现。
monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束出和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,并且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。
synchronized用的锁是存在哪里的
synchronized用的锁是存在Java对象头里的。
Java对象的内存布局:对象头(Header),实例数据(Instance Data)和对齐填充(Padding)。
https://blog.youkuaiyun.com/lkforce/article/details/81128115 对象头结构参考此文
对象头用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,这部分数据的长度在32位和64位的虚拟机(暂不考虑开启压缩指针的场景)中分别为32个和64个Bits,官方称它为“Mark Word”。
锁存在Java对象头里。如果对象是数组类型,则虚拟机用3个Word(字宽)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。32位系统上占用8bytes,64位系统上占用16bytes;即分别为32bit、64bit。
Java对象头里的Mark Word里默认存储对象的HashCode、分代年龄和锁标记位,在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。对于不同的锁及其状态而言,4个字节不足以表示其信息,所以按照锁标志位的不同,来存储不同的信息。
锁的升级与对比
在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。这几个状态会随着竞争情况逐渐升级。锁可以升级但不可以降级 目的是为了提高获得锁和释放锁的效率。
1. 偏向锁
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储偏向锁的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁,如果测试成功,则获得锁;若测试失败,则检测Mark Word中锁标志位是否设置为01,如果为01,则进行CAS操作将对象头偏向锁存储的线程ID指向当前线程,若不为01,则使用CAS竞争锁
偏向锁的撤销
偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。并且要等待全局安全点。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或标记对象不适合作为偏向锁,最后唤醒暂停的线程。
关闭偏向锁
偏向所在Java6和Java7里是默认开启的,但是它在应用程序启动几秒钟后才激活,如果必要刻意使用JVM参数来关闭延迟:-XX:BiasedLockingStartUpDelay=0.或直接关闭偏向锁:-XX:-UseBiasedLocking=false。
2. 轻量级锁
轻量级锁加锁
线程在执行同步块之前,JVM会现在当前的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
轻量级锁解锁
在解锁时,会使用原子的CAS操作将DIsplaced Mark Word替换回对象头,如果成功,则表示没有竞争发生。如果失败,则表示当前所存在竞争,锁会膨胀为重量级锁。竞争该锁的线程全部会阻塞,直到当前线程释放该锁。
锁的优缺点
2.3 原子操作的实现原理
原子操作意为“不可被中断的一个或者一系列的操作”。在多处理器上实现原子操作就变有点复杂。
处理器如何实现原子操作
1. 使用总线锁保证原子性: 所谓总线锁就是使用处理器提供的一个LOCK#的信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享变量。
2. 使用缓存保证原子性: 当一个处理器对缓存行中的共享变量进行操作时,通过缓存一致性协议,让其他处理器中缓存中的该共享变量无效,从而保证操作的原子性。因为缓存一致性会阻止同时修改由两个以上处理器缓存的内存区域的数据,当其他处理器回写已被锁定的缓存行的数据时,会使缓存无效。
有两种情况下处理器不会使用缓存锁定:
第一种情况是:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行时,则处理器会调用总线锁定。
第二种情况是:有些处理器不支持缓存锁定。
JAVA如何实现原子操作
在JAVA中可以通过锁或者循环CAS的方式来实现原子操作
1.通过循环CAS实现原子操作: 循环进行CAS操作直到成功为止。
CAS实现原子操作的三大问题:
ABA问题: 若共享变量修改过程为A->B->A,虽然值发生过更改,但使用CAS进行检查时会发现它的值没有发生变化。解决方法是在变量前追加版本号。1A->2B->3A。
循环时间长开销大:自旋CAS如果长时间不成功,会给CPU带来很大的开销。
只能保证一个共享变量的原子操作:对多个共享变量操作时,循环CAS就无法保证操作的原子性。
2. 使用锁机制实现原子操作: 保证只有获得锁的线程才能操作锁定的内存区域。 JVM内部实现了很多种锁机制,有偏向锁、轻量级锁和互斥锁。除了偏向锁,JVM实现锁的方式都用了循环CAS,即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS释放锁。