Synchronized基本概念

先了解几个术语:

  • 临界区: 指的是某⼀块代码区域块,它同⼀时刻只能由⼀个线程执⾏。
  • 竞态条件:对共享内存的进行竞争读写的地方
  • CAS(Compare and Swap ): 比较并交换,⽤于在硬件层⾯上提供原子性操作。 乐观锁就是用到了CAS

CAS

下面介绍CAS实现原子操作的两大问题

  • ABA问题:ABA问题就是⼀个值原来是A,变成了B,⼜变回了A。这个时候使⽤CAS是 检查不出变化的,但实际上却被更新了两次。 解决思路是使用“版本号”或者加上时间戳(JUC的atom包有响应的工具类可以使用)
  • 自旋开销大: CAS多与自旋结合。如果⾃旋CAS⻓时间不成功,会占⽤⼤量的CPU资源。 解决思路是让JVM⽀持处理器提供的pause指令, pause指令能让⾃旋失败时cpu睡眠⼀⼩段时间再继续⾃旋

CAS+volatile实现无锁

  1. class AccountSafe implements Account {
  2. private AtomicInteger balance; // 原子整数,用volatile实现的
  3. public AccountSafe(Integer balance) {
  4. this.balance = new AtomicInteger(balance);
  5. }
  6. @Override
  7. public Integer getBalance() {
  8. return balance.get();
  9. }
  10. @Override
  11. public void withdraw(Integer amount) {
  12. while (true) {
  13. int prev = balance.get();
  14. int next = prev - amount;
  15. if (balance.compareAndSet(prev, next)) { // cas
  16. break;
  17. }
  18. }
  19. // 可以简化为下面的方法
  20. // balance.addAndGet(-1 * amount);
  21. }
  22. }
  • CAS必须借助volatile才能读取到共享变量的最新值来实现【比较并交换】的效果

image.png


Synchronized使用

  1. // 关键字在实例⽅法上,锁为当前实例
  2. public synchronized void instanceLock() {
  3. // code
  4. }
  5. // 关键字在静态⽅法上,锁为当前Class对象
  6. public static synchronized void classLock() {
  7. // code
  8. }
  9. // 关键字在代码块上,锁为括号⾥⾯的对象
  10. public void blockLock() {
  11. Object o = new Object();
  12. synchronized (o) {
  13. // code
  14. }
  15. }

Synchronized实现原理

synchronized关键字的实现,依赖于Java的对象头中的Mark Word
image.png
Mark Word具体字段:
image.png

  • 默认存储对象的 HashCode、分代年龄、偏向模式以及锁标志位

偏向锁

我把偏向锁看成是一种对轻量级锁的优化
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行CAS操作。而偏向锁在进入之前可以判断进程id是否是自己,如果是则不用进行CAS操作
偏向锁使用: 以先判断锁标志,再判断偏向锁标志位,只有最后三位是 101才开始,否则直接走其他的锁。

  • 如果是匿名状态,线程ID为0,采⽤CAS去将当前线程写入,如果成功则获得锁,不成功表示存在竞争。
  • 线程ID不为0,此前已经有偏向,判断此值是否和当前线程相同,若⼀致则表示线程之前就获得了锁,不⼀致就尝试CAS替换。

如何撤销偏向锁:

  1. 如果有线程想要竞争该锁,就会撤销偏向锁
  2. 调用该对象的hashcode方法
  3. 调用wait/ notify方法

偏向锁的优缺点

  • 优点:只有单一线程访问对象的时候,偏向锁性能好,只在第一次需要CAS操作
  • 缺点:当发生竞争时,竞争需要等到安全点并且进行一系列比较,比较耗费时间。另外,当程序需要Hash值时,会调用HashCode方法,这会导致偏向锁退出

轻量级锁

  • 轻量级锁设计的初衷是在没有多线程竞争的前提 下,减少传统的重量级锁的资源损耗
  • 当锁标志位是00时,虚拟机栈就会开辟Lock Record空间

轻量级锁的使用

不再像偏向锁一样用线程id辩识,而是在当前线程的栈帧中建立⼀个指针,指向Lock Record空间
此指针空间包含两部分:

  1. displaced mark word:⽤于存储锁对象目前的Mark Word的拷贝副本
  2. owner:指向当前的锁对象的指针

虚拟即首先会将对象头的Mark World拷贝到栈帧中的Lock Record,如果操作成功则代表拥有了这个对象锁,如果操作失败则可能升级为重量级锁

轻量级锁的释放

使⽤CAS尝试将Lock Record中的displaced mark word替换回去,需要检查对象头中的指针是否依旧指向当前线程。 如果替换成功,表示没有竞争,锁成功释放
image.png

轻量级锁的重入

轻量级锁的每⼀次重入,都会在栈中⽣成⼀个Lock Record。第一次加锁时会拷贝Mark Word,Owner区指向对象头。后面的加锁(重入)并不会拷贝,仅仅是Owner指向对象本身, 每加⼀次锁帧栈中多⼀个Lock Record
image.png

自旋锁

  • 可以把自旋锁看成对锁膨胀的一种优化,每次轻量级锁发送竞争就进入重量级锁的话,很浪费性能,这时候先让线程多自旋几次,说不定这过程中,持有锁的线程也执行完毕了。
  • 自旋后之后仍未获得锁或者自旋的锁超过1个,那么会升级为重量级锁
  • JDK1.6之后加入了自适应自旋

重量级锁

重量级锁的实现依赖于ObjectMonitor,⽽ObjectMonitor⼜依赖于操作系统底层的Mutex Lock(互斥锁)实现
每个对象都可以关联一个Monitor对象(又叫管程、监视器),如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word中就被设置指向Monitor对象的指针
当线程访问同步代码块时,每个线程都会被封装成⼀个ObjectWaiter对象进⼊monitor

持有同一个Monitor的对象会按以下规则运行:
image.png

  • 正在执行的线程称为Owner,如果这时候有另一个线程想要进入临界区,就会被存放在EntryList中,进行阻塞
  • 当Owner线程执行完毕后,会唤醒EntryList的线程(非公平竞争)
  • WaitSet是之前获得到锁,但条件不满足进入Waiting状态的线程

重量级的优缺点

  • 缺点:重量级锁需要调用内核空间,产生较大开销。
  • 优点:重量级锁可以应对竞争激烈的场景