以下内容来自《Java并发编程》书籍第二章。
⚠️补充:
1、volatile的有序性
volatile通过内存屏障实现禁止指令重排序保证有序性。硬件层面的内存屏障分为Load Barrier 和 Store Barrier即读屏障和写屏障。
2、同步代码块和同步方法底层实现是有区别的。
一个使用标识位,一个使用监控。
同步代码块是在代码块的开始和结束或异常位置插入monitorenter和monitorexit指令。
1、如果monitor的进入数为0,则线程获取锁,并设置monitor的进入数为1
2、如果该线程已经占有该monitor,则进入数+1(同一个线程需要获取多次monitor对象,同一个对象中不同的同步代码块执行)--可重入锁
3、如果其他线程占有该monitor,则monitor的进入数不为0,则该线程进入阻塞状态,直到monitor为0,重新获取monitor的所有权
同步方法是有一个ACC_SYNCHRONIZRED标志,调用方法时会检查这个标志,如果被设置了,执行线程先获取monitor,再执行方法代码,再释放monitor,同一个monitor在同一个时刻只能由一个线程所有。
3、锁升级
首先来了解相关锁的概念:
自旋锁(CAS):无锁状态。让不满足条件的线程等待一会看能不能获得锁,通过占用处理器的时间来避免线程切换带来的开销。自旋等待的时间或次数是有一个限度的,如果自旋超过了定义的时间仍然没有获取到锁,则该线程应该被挂起。在 JDK1.6 之后,引入了自适应自旋锁,自适应意味着自旋的次数不是固定不变的,而是根据前一次在同一个锁上自旋的时间以及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
偏向锁:大多数情况下,锁总是由同一个线程多次获得。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,偏向锁是一个可重入的锁。如果锁对象头的Mark Word里存储着指向当前线程的偏向锁,无需重新进行CAS操作来加锁和解锁。当有其他线程尝试竞争偏向锁时,持有偏向锁的线程(不处于活动状态)才会释放锁。偏向锁无法使用自旋锁优化,因为一旦有其他线程申请锁,就破坏了偏向锁的假定进而升级为轻量级锁。
轻量级锁:减少无实际竞争情况下,使用重量级锁产生的性能消耗。JVM会现在当前线程的栈桢中创建用于存储锁记录的空间 LockRecord,将对象头中的 Mark Word 复制到 LockRecord 中并将 LockRecord 中的 Owner 指针指向锁对象。然后线程会尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针,成功则当前线程获取到锁,失败则表示其他线程竞争锁当前线程则尝试使用自旋的方式获取锁。自旋获取锁失败则锁膨胀升级为重量级锁。
重量级锁:通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实 现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。线程竞争不使用自旋,不会消耗CPU。但是线程会进入阻塞等待被其他线程被唤醒,响应时间缓慢。
锁升级过程: