普通同步方法,锁是当前实例对象
静态同步方法,锁是当前类的 class 对象
同步方法块,锁是括号里面的对象
同步代码块是使用 monitorenter 和 monitorexit 指令实现的;
同步方法(需要看JVM底层实现)依靠的是方法修饰符上的ACC_SYNCHRONIZED 实现
在 JVM 中 monitorenter 和 monitorexit 字节码依赖于底层的操作系统的Mutex Lock 来实现的,但是由于使用 Mutex Lock 需要将当前线程挂起并从用户态切换到内核态来执行,这种切换的代价是非常昂贵的。然而,在现实中的大部分情况下,同步方法是运行在单线程环境(无锁竞争环境),如果每次都调用 Mutex Lock 那么将严重的影响程序的性能。
JDK 1.6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
##Java对象
对象头,实例数据,对齐填充字节
##Java对象头
synchronized 用的锁是存在Java对象头里的
Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段,用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等)、Klass Pointer(类型指针,是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例)
Mark Word在32位JVM中的长度是32bit,在64位JVM中长度是64bit。
Java 对象头如果对象是数组类型,因为 JVM 虚拟机可以通过 Java 对象的元数据信息确定 Java 对象的大小,无法从数组的元数据来确认数组的大小,需要用一块来记录数组长度。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-501RWe9V-1616385487090)(https://www.showdoc.cc/server/api/common/visitfile/sign/bb9d83a2f9bac8ccac066eda74f5d0ca?showdoc=.jpg)]
Mark Word 被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据,它会根据对象的状态复用自己的存储空间,也就是说,Mark Word 会随着程序的运行发生变化
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l8BYAYtY-1616385487093)(https://www.showdoc.cc/server/api/common/visitfile/sign/4248cc7a6a6610b779a45f663dafcacf?showdoc=.jpg)]
##自旋锁
线程的阻塞和唤醒,需要 cpu 从用户态转为核心态。
所谓自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。执行一段无意义的循环即可(自旋)。虽然它可以避免线程切换带来的开销,但是它占用了处理器的时间。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好,反之,自旋的线程就会白白消耗掉处理的资源,它不会做任何有意义的工作。
自旋等待的时间(自旋的次数)必须要有一个限度,如果自旋超过了定义的时间仍然没有获取到锁,则应该被挂起。
自旋锁在 JDK 1.4.2 中引入,默认关闭,但是可以使用 -XX:+UseSpinning 开开启。
在 JDK1.6 中默认开启。同时自旋的默认次数为 10 次,可以通过参数 -XX:PreBlockSpin 来调整。
##适应自旋锁
所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。
反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。
有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测会越来越准确,虚拟机会变得越来越聪明
##锁消除
在有些情况下,JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进行锁消除。
锁消除可以节省毫无意义的请求锁的时间。
锁消除的依据是逃逸分析的数据支持。
我们在使用一些 JDK 的内置 API 时,如 StringBuffer、Vector、HashTable 等,这个时候会存在隐性的加锁操作。
在运行下面这段代码时,JVM 可以明显检测到变量 vector 没有逃逸出方法 #vectortest() 之外,所以 JVM 可以大胆地将 vector 内部的加锁操作消除。
public void vectortest(){
Vector<String> vector = new Vector<String>();
for (int i = 0 ; i < 10 ; i++){
vector.add(i + "");
}
System.out.println(vector);
}
##锁粗化
使用同步锁的时候,需要让同步块的作用范围尽可能小:仅在共享数据的实际作用域中才进行同步。这样做的目的,是为了使需要同步的操作数量尽可能缩小,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。但是如果一系列的连续加锁解锁操作,可能会导致不必要的性能损耗。
锁粗化,就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。
如vector 每次 add 的时候都需要加锁操作,JVM 检测到对同一个对象(vector)连续加锁、解锁操作,会合并一个更大范围的加锁、解锁操作,即加锁解锁操作会移到 for 循环之外。
##锁升级
- 无锁状态
- 偏向锁状态
- 轻量级锁状态
- 重量级锁(通过对象内部的监视器(Monitor)实现)
锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率
当关闭偏向锁功能或者多个线程竞争偏向锁,导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁
##偏向锁
为了在无多线程竞争的情况下,尽量减少不必要的轻量级锁执行路径
偏向锁在 JDK 1.6 以上,默认开启。开启后程序启动几秒后才会被激活,可使用 JVM 参数 -XX:BiasedLockingStartupDelay = 0 来关闭延迟。
如果确定锁通常处于竞争状态,则可通过JVM参数 -XX:-UseBiasedLocking=false 关闭偏向锁,那么默认会进入轻量级锁。
偏向锁只需要在置换 ThreadID 的时候依赖一次 CAS 原子指令,其余时刻不需要 CAS 指令(相比其他锁)。
一旦出现多线程竞争的情况就必须撤销偏向锁。
轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能
偏向锁的释放采用了一种只有竞争才会释放锁的机制,线程是不会主动去释放偏向锁,需要等待其他线程来竞争。
- 检测 Mark Word是 否为可偏向状态,即是否为偏向锁的标识位为 1 ,锁标识位为 01 。
- 若为可偏向状态,则测试线程 ID 是否为当前线程 ID ?如果是,则执行步骤(5);否则,执行步骤(3)。
- 如果线程 ID 不为当前线程 ID ,则通过 CAS 操作竞争锁。竞争成功,则将 Mark Word 的线程 ID 替换为当前线程 ID ,则执行步骤(5);否则,执行线程(4)。
- 通过 CAS 竞争锁失败,证明当前存在多线程竞争情况,当到达全局安全点,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块。
- 执行同步代码块
偏向锁的撤销需要等待全局安全点(这个时间点是上没有正在执行的代码)。
- 首先会暂停拥有偏向锁的线程并检查该线程是否存活:
如果线程非活动状态,则将对象头设置为无锁状态(其他线程会重新获取该偏向锁)。
如果线程是活动状态,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,并将对栈中的锁记录和对象头的 MarkWord 进行重置:
要么重新偏向于其他线程(即将偏向锁交给其他线程,相当于当前线程”被”释放了锁)
要么恢复到无锁或者标记锁对象不适合作为偏向锁(此时锁会被升级为轻量级锁)
- 最后唤醒暂停的线程,被阻塞在安全点的线程继续往下执行同步代码块
##轻量级锁-获取锁
- 判断当前对象是否处于无锁状态?
若是,则 JVM 首先将在当前线程的栈帧中,建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的 Mark Word的 拷贝(官方把这份拷贝加了一个 displaced 前缀,即 displaced Mark Word);
若否,执行步骤(3); - JVM 利用 CAS 操作尝试将对象的 Mark Word 更新为指向 Lock Record 的指正。
如果成功,表示竞争到锁,则将锁标志位变成 00(表示此对象处于轻量级锁状态),执行同步操作;
如果失败,则执行步骤(3); - 判断当前对象的 Mark Word 是否指向当前线程的栈帧?
如果是,则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;
如果否,说明该锁对象已经被其他线程抢占了,当前线程便尝试使用自旋来获取锁。若自旋后没有获得锁,此时轻量级锁会升级为重量级锁,锁标志位变成 10,当前线程会被阻塞。
##轻量级锁-释放锁
- 取出在获取轻量级锁保存在 displaced Mark Word 中 数据。
- 使用 CAS 操作将取出的数据替换当前对象的 Mark Word 中。如果成功,则说明释放锁成功。
- 唤醒被挂起的线程,重新争夺锁,访问同步代码块。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。