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实现无锁
class AccountSafe implements Account {
private AtomicInteger balance; // 原子整数,用volatile实现的
public AccountSafe(Integer balance) {
this.balance = new AtomicInteger(balance);
}
@Override
public Integer getBalance() {
return balance.get();
}
@Override
public void withdraw(Integer amount) {
while (true) {
int prev = balance.get();
int next = prev - amount;
if (balance.compareAndSet(prev, next)) { // cas
break;
}
}
// 可以简化为下面的方法
// balance.addAndGet(-1 * amount);
}
}
- CAS必须借助volatile才能读取到共享变量的最新值来实现【比较并交换】的效果
Synchronized使用
// 关键字在实例⽅法上,锁为当前实例
public synchronized void instanceLock() {
// code
}
// 关键字在静态⽅法上,锁为当前Class对象
public static synchronized void classLock() {
// code
}
// 关键字在代码块上,锁为括号⾥⾯的对象
public void blockLock() {
Object o = new Object();
synchronized (o) {
// code
}
}
Synchronized实现原理
synchronized关键字的实现,依赖于Java的对象头中的Mark Word
Mark Word具体字段:
- 默认存储对象的 HashCode、分代年龄、偏向模式以及锁标志位
偏向锁
我把偏向锁看成是一种对轻量级锁的优化
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行CAS操作。而偏向锁在进入之前可以判断进程id是否是自己,如果是则不用进行CAS操作
偏向锁使用: 以先判断锁标志,再判断偏向锁标志位,只有最后三位是 101才开始,否则直接走其他的锁。
- 如果是匿名状态,线程ID为0,采⽤CAS去将当前线程写入,如果成功则获得锁,不成功表示存在竞争。
- 线程ID不为0,此前已经有偏向,判断此值是否和当前线程相同,若⼀致则表示线程之前就获得了锁,不⼀致就尝试CAS替换。
如何撤销偏向锁:
- 如果有线程想要竞争该锁,就会撤销偏向锁
- 调用该对象的hashcode方法
- 调用wait/ notify方法
偏向锁的优缺点
- 优点:只有单一线程访问对象的时候,偏向锁性能好,只在第一次需要CAS操作
- 缺点:当发生竞争时,竞争需要等到安全点并且进行一系列比较,比较耗费时间。另外,当程序需要Hash值时,会调用HashCode方法,这会导致偏向锁退出
轻量级锁
- 轻量级锁设计的初衷是在没有多线程竞争的前提 下,减少传统的重量级锁的资源损耗
- 当锁标志位是00时,虚拟机栈就会开辟Lock Record空间
轻量级锁的使用
不再像偏向锁一样用线程id辩识,而是在当前线程的栈帧中建立⼀个指针,指向Lock Record空间
此指针空间包含两部分:
- displaced mark word:⽤于存储锁对象目前的Mark Word的拷贝副本
- owner:指向当前的锁对象的指针
虚拟即首先会将对象头的Mark World拷贝到栈帧中的Lock Record,如果操作成功则代表拥有了这个对象锁,如果操作失败则可能升级为重量级锁
轻量级锁的释放
使⽤CAS尝试将Lock Record中的displaced mark word替换回去,需要检查对象头中的指针是否依旧指向当前线程。 如果替换成功,表示没有竞争,锁成功释放
轻量级锁的重入
轻量级锁的每⼀次重入,都会在栈中⽣成⼀个Lock Record。第一次加锁时会拷贝Mark Word,Owner区指向对象头。后面的加锁(重入)并不会拷贝,仅仅是Owner指向对象本身, 每加⼀次锁帧栈中多⼀个Lock Record
自旋锁
- 可以把自旋锁看成对锁膨胀的一种优化,每次轻量级锁发送竞争就进入重量级锁的话,很浪费性能,这时候先让线程多自旋几次,说不定这过程中,持有锁的线程也执行完毕了。
- 自旋后之后仍未获得锁或者自旋的锁超过1个,那么会升级为重量级锁
- JDK1.6之后加入了自适应自旋
重量级锁
重量级锁的实现依赖于ObjectMonitor,⽽ObjectMonitor⼜依赖于操作系统底层的Mutex Lock(互斥锁)实现
每个对象都可以关联一个Monitor对象(又叫管程、监视器),如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word中就被设置指向Monitor对象的指针
当线程访问同步代码块时,每个线程都会被封装成⼀个ObjectWaiter对象进⼊monitor
持有同一个Monitor的对象会按以下规则运行:
- 正在执行的线程称为Owner,如果这时候有另一个线程想要进入临界区,就会被存放在EntryList中,进行阻塞
- 当Owner线程执行完毕后,会唤醒EntryList的线程(非公平竞争)
- WaitSet是之前获得到锁,但条件不满足进入Waiting状态的线程
重量级的优缺点
- 缺点:重量级锁需要调用内核空间,产生较大开销。
- 优点:重量级锁可以应对竞争激烈的场景