1、Volatile的应用

轻量级的synchronized,它在多核处理器开发中保证了共享变量的“可见性”。
可见性:当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
实现原理:Lock前缀指令(汇编后可以看到)。
1)将当前处理器缓存行的数据写回到系统内存。
2)这个写回内存的操作会使其他CPU里缓存了该内存地址的数据无效。

为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1/L2/L3)后再进行操作,但操作完不知道何时会写到内存。
如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀指令,将这个变量所在缓存行的数据写回到系统内存。
但是如果其他处理器缓存的值还是旧的,再去执行计算操作就会有问题。
答案:缓存一致性协议,每个处理器通过嗅探总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。

2、synchronized实现原理与应用

对象锁,Java中每一个对象都可以作为锁。
1)对于普通同步方法,锁是当前实例对象。
2)对于静态同步方法,锁是当前类的Class对象。
3)对于同步代码块,锁是Synchronized()括号里配置的对象。

实现原理:**JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。但两者实现的细节不一样, 代码块同步是使用Monitorenter和Monitorexit指令实现的,而方法的同步是使用另外一种方式实现的,细节JVM规范里并没有详细说明。

监视器锁(Monitor 另一个名字叫管程)本质是依赖于底层的操作系统的 Mutex Lock(互斥锁)来实现的。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如 monitor 可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。

mutex 的工作方式
在 Java 虚拟机 (HotSpot) 中,Monitor 是基于 C++ 实现的,由 ObjectMonitor 实现的, 几个关键属性:

  • _owner:指向持有 ObjectMonitor 对象的线程
  • _WaitSet:存放处于 wait 状态的线程队列
  • _EntryList:存放处于等待锁 block 状态的线程队列
  • _recursions:锁的重入次数
  • count:用来记录该线程获取锁的次数

第二章Java并发机制的底层实现原理 - 图1
ObjectMonitor 中有两个队列,_WaitSet 和 _EntryList,用来保存 ObjectWaiter 对象列表( 每个等待锁的线程都会被封装成 ObjectWaiter 对象),_owner 指向持有 ObjectMonitor 对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的 monitor 后进入 _Owner 区域并把 monitor 中的 owner 变量设置为当前线程同时 monitor 中的计数器 count 加 1。
若线程调用 wait() 方法,将释放当前持有的 monitor,owner 变量恢复为 null,count 自减 1,同时该线程进入 WaitSet 集合中等待被唤醒。若当前线程执行完毕也将释放 monitor(锁)并复位变量的值,以便其他线程进入获取 monitor(锁)。

在 Java 中,最基本的互斥同步手段就是 synchronized,经过编译之后会在同步块前后分别插入 monitorenter, monitorexit 这两个字节码指令,而这两个字节码指令都需要提供一个 reference 类型的参数来指定要锁定和解锁的对象,具体表现如下所示:

  • 在普通同步方法,reference 关联和锁定的是当前方法示例对象;
  • 对于静态同步方法,reference 关联和锁定的是当前类的 class 对象;
  • 在同步方法块中,reference 关联和锁定的是括号里制定的对象;

Java 对象头

synchronized 用的锁存在 Java 对象头里,在 JVM 中,对象在内存的布局分为三块区域:对象头、实例数据、对齐填充。
第二章Java并发机制的底层实现原理 - 图2对象头

  • 对象头:MarkWord 和 metadata,也就是图中对象标记和元数据指针;
  • 实例对象:存放类的属性数据,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按 4 字节对齐;
  • 填充数据:由于虚拟机要求对象起始地址必须是 8 字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐;

对象头是 synchronized 实现的关键,synchronized 使用的锁对象是存储在 Java 对象头里的,jvm 中采用 2 个字宽(一个字宽代表 4 个字节,一个字节 8bit)来存储对象头(如果对象是数组则会分配 3 个字宽,多出来的 1 个字宽记录的是数组长度)。其主要结构是由 Mark Word 和 Class Metadata Address 组成。
synchronized(lock) 中的 lock 可以用 Java 中任何一个对象来表示,而锁标识的存储实际上就是在 lock 这个对象中的对象头内。
Monitor(监视器锁)本质是依赖于底层的操作系统的 Mutex Lock(互斥锁)来实现的。Mutex Lock 的切换需要从用户态转换到核心态中,因此状态转换需要耗费很多的处理器时间。所以 synchronized 是 Java 语言中的一个重量级操作。

为什么任意一个 Java 对象都能成为锁对象呢?
Java 中的每个对象都派生自 Object 类,而每个 Java Object 在 JVM 内部都有一个 native 的 C++对象 oop/oopDesc 进行对应。其次,线程在获取锁的时候,实际上就是获得一个监视器对象(monitor) ,monitor 可以认为是一个同步对象,所有的 Java 对象是天生携带 monitor。
多个线程访问同步代码块时,相当于去争抢对象监视器修改对象中的锁标识, ObjectMonitor 这个对象和线程争抢锁的逻辑有密切的关系。

锁的存储结构

synchronized用的锁是存在Java对象头里的,JAVA对象头包括了两个部分。
1、Mark Word:存储对象自身的运行时数据,如哈希码(HashCode) 、 GC分代年龄、 锁状态标志、 线程持有的锁、 偏向线程ID、 偏向时间戳等。
2、类型指针:对象指向它的类型元数据的指针,java虚拟机通过这个指针来确定该对象是哪个类的实例。
image.png
锁标志位的表示意义:

  1. 锁标识 lock=00 表示轻量级锁
  2. 锁标识 lock=10 表示重量级锁
  3. 偏向锁标识 biased_lock=1 表示偏向锁
  4. 偏向锁标识 biased_lock=0 且锁标识=01 表示无锁状态

    升级过程

    无锁状态—>偏向锁—>轻量级锁—>重量级锁,锁可以升级但不能降级,目的是为了提高获得锁和释放锁的效率。

偏向锁

当一个线程访问同步块并获取锁时,会在对象头和栈帧中锁记录里存储偏向的线程ID,以后该线程在进入和退出同步块时时不需要进行CAS操作来加锁和解锁,只需要简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

偏向锁的撤销

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录 ,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。
如图:
线程1演示了偏向锁的初始化流程,线程2演示了偏向锁的撤销流程。
clipboard.png

关闭偏向锁

偏向锁在Java6和Java7里是默认启用的,但是它们在应用程序启动几秒钟后才激活。
关闭延迟:-XX:biasedLockingStartupDelay=0;
关闭偏向锁:-XX:-UseBiasedLocking=false;

轻量级锁

轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。

加锁

线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

解锁

轻量级锁解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头 ,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
如图:clipboard.png
注:因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态,当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁值之争。

重量级锁

直接阻塞线程。线程竞争不适用CPU自旋,适用于:很多线程竞争锁,且锁持有时间长,追求吞吐量的场景。

重入锁

重入是当一个线程进入了这个类实例的方法,这个线程再去用别的方法时不会被锁住因为,因为是同一个线程,所以会重新进入不会被阻塞。

自旋锁

一直在cpu旋转

死锁

多个线程都在等待对方释放锁资源,比如一个线程拿到了A锁,这边要调用B锁,另一个线程拿到了B锁,这边要调用A锁,对方在等它释放互相调用都没有释放则会出现死锁。

  • 避免一个线程获取多个锁
  • 避免一个线程同时占用多个共享资源,尽量每个锁占用一个资源。
  • 尝试使用定时锁,使用lock.tryLock(timeout)来替代内置锁


**