volatile&偏向锁的原理及过程

synchronized锁

1.用法

  1. 修饰代码块,称为同步块,作用范围就是整个一个代码块
synchronized(data){
      data.add(i);
  }
  1. 修饰一个非静态方法,被修饰的方法称为同步方法,作用范围为整个方法
pulic synchronized void add(int i){
	data.add(i);
}
  1. 修饰一个静态方法,作用范围为整个静态方法
pulic synchronized static void add(int i){
	data.add(i);
}

在 JVM 中,对象在内存中分为三块区域:

  • 对象头
    Mark Word(标记字段):默认存储对象的HashCode,分代年龄和锁标志位信息。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。
    Klass Point(类型指针):对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
  • 实例数据
    这部分主要是存放类的数据信息,父类的信息。
    对其填充
    由于虚拟机要求对象起始地址必须是8字节的整数倍,填充数据不是必须存在的,仅仅是为了字节对齐。

首先如果使用synchronized,它会关联到一个monitor对象 --> 如果关联那就看是否与Owner绑定,如果绑定就进入阻塞队列中

volatile

  • volatile是轻量级的synchronized,主要保证两件事可见性和禁止指令重排
  • 可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。

如何保证可见性呢?

Java代码如下。

instance = new Singleton(); // instance是volatile变量
转变成汇编代码,如下。
0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);

有volatile变量修饰的共享变量进行写操作的时候会多出第二行汇编代码,通过查IA-32架
构软件开发者手册可知,Lock前缀的指令在多核处理器下会引发了两件事情 [1] 。
1)将当前处理器缓存行的数据写回到系统内存。
2)这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。

简单聊一聊 as-if-serial

1.不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。
2.as-if-serial语义把单线程程序保护了起来,遵守as-if-serial语义的编译器、runtime和处理器
共同为编写单线程程序的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的。as-if-serial语义使单线程程序员无需担心重排序会干扰他们,也无需担心内存可见性问题。

下面来具体讲解volatile的两条实现原则。

1. Lock前缀指令会引起处理器缓存回写到内存。Lock前缀指令导致在执行指令期间,声言处理器的LOCK#信号。在多处理器环境中,LOCK#信号确保在声言该信号期间,处理器可以独占任何共享内存 [2] 。但是,在最近的处理器里,LOCK#信号一般不锁总线,而是锁缓存,毕竟锁总线开销的比较大。在8.1.4节有详细说明锁定操作对处理器缓存的影响,对于Intel486和Pentium处理器,在锁操作时,总是在总线上声言LOCK#信号。但在P6和目前的处理器中,如果访问的内存区域已经缓存在处理器内部,则不会声言LOCK#信号。相反,它会锁定这块内存区域的缓存并回写到内存,并使用缓存一致性机制来确保修改的原子性,此操作被称为“缓存锁定”,缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据。

2)一个处理器的缓存回写到内存会导致其他处理器的缓存无效。IA-32处理器和Intel 64处理器使用MESI(修改、独占、共享、无效)控制协议去维护内部缓存和其他处理器缓存的一致性。在多核处理器系统中进行操作的时候,IA-32和Intel 64处理器能嗅探其他处理器访问系统内存和它们的内部缓存。处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致。例如,在Pentium和P6 family处理器中,如果通过嗅探一个处理器来检测其他处理器打算写内存地址,而这个地址当前处于共享状态,那么正在嗅探的处理器将使它的缓存行无效,在下次访问相同内存地址时,强制执行缓存行填充。

优化volatile

在JDK 7的并发包里新增一个队列集合类Linked-TransferQueue,它在使用volatile变量时,用一种追加字节的方式来优化队列出队和入队的性能。LinkedTransferQueue的代码如下。

/** 队列中的头部节点 */
private transient f?inal PaddedAtomicReference<QNode> head;
/** 队列中的尾部节点 */
private transient f?inal PaddedAtomicReference<QNode> tail;
static f?inal class PaddedAtomicReference <T> extends AtomicReference T> {
// 使用很多4个字节的引用追加到64个字节
Object p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, pa, pb, pc, pd, pe;
    PaddedAtomicReference(T r) {
    super(r);
  }
}
public class AtomicReference <V> implements java.io.Serializable {
private volatile V value;
    // 省略其他代码
}
  • LinkedTransferQueue这个类,它使用一个内部类类型来定义队列的
    头节点(head)和尾节点(tail),而这个内部类PaddedAtomicReference相对于父类
    AtomicReference只做了一件事情,就是将共享变量追加到64字节。我们可以来计算下,一个对
    象的引用占4个字节,它追加了15个变量(共占60个字节),再加上父类的value变量,一共64个
    字节。

偏向锁

一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
偏向锁是等到竞争出现才释放锁的机制-->当其他线程尝试竞争偏向锁的时候-->持有偏向锁的线程才会释放锁;
偏向锁的撤销:
  • 首先那等待全局安全点-->暂停拥有偏向锁的线程(检查是否还活着)-->如果活着,偏向锁的栈就会执行-->便利偏向锁对象的锁记录-->占中的所对象和对象头的Mark Word要么重新偏向于其他线程-->要么恢复到无所活着标记对象不适合作为偏向锁-->最后唤醒暂停的线程

轻量级锁解锁

会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。

锁的优缺点对比

遇到的面试题:

1.volatile与synchronized的区别

  • volatile只能修饰实例变量和类变量,而synchronized可以修饰方法,以及代码块。
    volatile保证数据的可见性,但是不保证原子性(多线程进行写操作,不保证线程安全);而synchronized是一种排他(互斥)的机制。
  • volatile用于禁止指令重排序:可以解决单例双重检查对象初始化代码执行乱序问题。
  • volatile可以看做是轻量版的synchronized,volatile不保证原子性,但是如果是对一个共享变量进行多个线程的赋值,而没有其他的操作,那么就可以用volatile来代替synchronized,因为赋值本身是有原子性的,而volatile又保证了可见性,所以就可以保证线程安全了。

2.synchronized和Lock区别

  • synchronized是关键字,是JVM层面的底层啥都帮我们做了,而Lock是一个接口,是JDK层面的有丰富的API。
  • synchronized会自动释放锁,而Lock必须手动释放锁。
  • synchronized是不可中断的,Lock可以中断也可以不中断。
  • 通过Lock可以知道线程有没有拿到锁,而synchronized不能。
  • synchronized能锁住方法和代码块,而Lock只能锁住代码块。
  • Lock可以使用读锁提高多线程读效率。
  • synchronized是非公平锁,ReentrantLock可以控制是否是公平锁。
posted @ 2021-05-10 08:02  xiaoff  阅读(246)  评论(0编辑  收藏  举报