JAVA各种锁的优劣对比分析( 三 )


在自旋锁中 另有三种常见的锁形式:TicketLock、CLHlock和MCSlock , 本文中仅做名词介绍 , 不做深入讲解 , 感兴趣的同学可以自行查阅相关资料 。
3. 无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁这四种锁是指锁的状态 , 专门针对synchronized的 。在介绍这四种锁状态之前还需要介绍一些额外的知识 。
首先为什么Synchronized能实现线程同步?
在回答这个问题之前我们需要了解两个重要的概念:“Java对象头”、“Monitor” 。
Java对象头
synchronized是悲观锁 , 在操作同步资源之前需要给同步资源先加锁 , 这把锁就是存在Java对象头里的 , 而Java对象头又是什么呢?
我们以Hotspot虚拟机为例 , Hotspot的对象头主要包括两部分数据:Mark word(标记字段)、Klass Pointer(类型指针) 。
Mark Word:默认存储对象的HashCode , 分代年龄和锁标志位信息 。这些信息都是与对象自身定义无关的数据 , 所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据 。它会根据对象的状态复用自己的存储空间 , 也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化 。
Klass Point:对象指向它的类元数据的指针 , 虚拟机通过这个指针来确定这个对象是哪个类的实例 。
Monitor
Monitor可以理解为一个同步工具或一种同步机制 , 通常被描述为一个对象 。每一个Java对象就有一把看不见的锁 , 称为内部锁或者Monitor锁 。
Monitor是线程私有的数据结构 , 每一个线程都有一个可用monitor record列表 , 同时还有一个全局的可用列表 。每一个被锁住的对象都会和一个monitor关联 , 同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识 , 表示该锁被这个线程占用 。
现在话题回到synchronized , synchronized通过Monitor来实现线程同步 , Monitor是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的线程同步 。
如同我们在自旋锁中提到的“阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成 , 这种状态转换需要耗费处理器时间 。如果同步代码块中的内容过于简单 , 状态转换消耗的时间有可能比用户代码执行的时间还要长” 。这种方式就是synchronized最初实现同步的方式 , 这就是JDK 6之前synchronized效率低的原因 。这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁” , JDK 6中为了减少获得锁和释放锁带来的性能消耗 , 引入了“偏向锁”和“轻量级锁” 。
所以目前锁一共有4种状态 , 级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁 。锁状态只能升级不能降级 。
通过上面的介绍 , 我们对synchronized的加锁机制以及相关知识有了一个了解 , 那么下面我们给出四种锁状态对应的的Mark Word内容 , 然后再分别讲解四种锁状态的思路以及特点:

JAVA各种锁的优劣对比分析

文章插图
 
无锁
无锁没有对资源进行锁定 , 所有的线程都能访问并修改同一个资源 , 但同时只有一个线程能修改成功 。
无锁的特点就是修改操作在循环内进行 , 线程会不断的尝试修改共享资源 。如果没有冲突就修改成功并退出 , 否则就会继续循环尝试 。如果有多个线程修改同一个值 , 必定会有一个线程能修改成功 , 而其他修改失败的线程会不断重试直到修改成功 。上面我们介绍的CAS原理及应用即是无锁的实现 。无锁无法全面代替有锁 , 但无锁在某些场合下的性能是非常高的 。
偏向锁
偏向锁是指一段同步代码一直被一个线程所访问 , 那么该线程会自动获取锁 , 降低获取锁的代价 。
在大多数情况下 , 锁总是由同一线程多次获得 , 不存在多线程竞争 , 所以出现了偏向锁 。其目标就是在只有一个线程执行同步代码块时能够提高性能 。
当一个线程访问同步代码块并获取锁时 , 会在Mark Word里存储锁偏向的线程ID 。在线程进入和退出同步块时不再通过CAS操作来加锁和解锁 , 而是检测Mark Word里是否存储着指向当前线程的偏向锁 。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径 , 因为轻量级锁的获取及释放依赖多次CAS原子指令 , 而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令即可 。


推荐阅读