https://blog.csdn.net/qq_33375499/article/details/105161343 Synchnized和lock的区别 Synchronized是悲观锁 CAS是乐观锁
锁的状态
无锁->偏向锁->轻量级锁->重量级锁![](/i/l/?n=23&i=blog/1773218/202303/1773218-20230318153146180-410613742.png)
锁对比
锁 | 优点 | 缺点 | 适用场景 |
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到索竞争的线程,使用自旋会消耗CPU | 追求响应速度,同步块执行速度非常快 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,同步块执行速度较慢 |
Synchronized锁
synchronized
用的锁是存在Java对象头里的,那么什么是对象头呢?
Java 对象头
我们以 Hotspot 虚拟机为例,Hopspot 对象头主要包括两部分数据:Mark Word(标记字段) 和 Klass Pointer(类型指针)
Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。
Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
在上面中我们知道了,synchronized
用的锁是存在Java对象头里的,那么具体是存在对象头哪里呢?答案是:存在锁对象的对象头的Mark Word中,那么MarkWord在对象头中到底长什么样,它到底存储了什么呢?
在64位的虚拟机中:
![](/i/l/?n=23&i=blog/1773218/202303/1773218-20230318153146128-1939405680.png)
![](/i/l/?n=23&i=blog/1773218/202303/1773218-20230318153145962-1194906809.png)
- age_bits: 就是我们说的分代回收的标识,占用4字节
- lock_bits: 是锁的标志位,占用2个字节
- biased_lock_bits: 是是否偏向锁的标识,占用1个字节
- max_hash_bits: 是针对无锁计算的hashcode 占用字节数量,如果是32位虚拟机,就是 32 - 4 - 2 -1 = 25 byte,如果是64 位虚拟机,64 - 4 - 2 - 1 = 57 byte,但是会有 25 字节未使用,所以64位的 hashcode 占用 31 byte
- hash_bits: 是针对 64 位虚拟机来说,如果最大字节数大于 31,则取31,否则取真实的字节数
- cms_bits: 不是64位虚拟机就占用 0 byte,是64位就占用 1byte
- epoch_bits: 就是 epoch 所占用的字节大小,2字节。
5.2 Monitor
Monitor 可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个 Java 对象就有一把看不见的锁,称为内部锁或者 Monitor 锁。 Monitor 是线程私有的数据结构,每一个线程都有一个可用 monitor record 列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个 monitor 关联,同时 monitor 中有一个 Owner 字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。Synchronized
是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的 Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么 Synchronized 效率低的原因。因此,这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为重量级锁。
随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。JDK 1.6中默认是开启偏向锁和轻量级锁的,我们也可以通过-XX:-UseBiasedLocking=false来禁用偏向锁。
锁的分类
无锁
无锁是指没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。 无锁的特点是修改操作会在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。偏向锁
初次执行到synchronized代码块的时候,锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁。当第二次到达同步代码块时,线程会判断此时持有锁的线程是否就是自己(持有锁的线程ID也在对象头里),如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。 偏向锁是指当一段同步代码一直被同一个线程所访问时,即不存在多个线程的竞争时,那么该线程在后续访问时便会自动获得锁,从而降低获取锁带来的消耗,即提高性能。 当一个线程访问同步代码块并获取锁时,会在 Mark Word 里存储锁偏向的线程 ID。在线程进入和退出同步块时不再通过 CAS 操作来加锁和解锁,而是检测 Mark Word 里是否存储着指向当前线程的偏向锁。轻量级锁的获取及释放依赖多次 CAS 原子指令,而偏向锁只需要在置换 ThreadID 的时候依赖一次 CAS 原子指令即可。 偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。偏向锁
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的MarkWord里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下MarkWord中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程,如果失败则进行轻量锁的升级。轻量级锁(自旋锁)
![](/i/l/?n=23&i=blog/1773218/202303/1773218-20230318153146119-1886761202.png)