代码中写了一个synchronized
之后, 可能会产生一系列的"自适应过程", 叫锁升级, 也称为锁膨胀.
过程: 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
第一个尝试加锁的线程, 优先进入偏向锁状态.
这里不是真的加锁, 只是给锁对象头中做一个 “偏向锁的标记”, 记录这个锁属于哪个线程.
如果没有别的线程竞争锁, 那就不会加锁了(避免了加锁解锁的开销);
如果后续有其他线程来竞争该锁(刚才已经在锁对象中记录了当前锁属于哪个线程了, 很容易识别当前申请锁的线程是不是之前记录的线程), 那就取消原来的偏向锁状态, 进入一般的轻量级锁状态.
偏向锁本质上相当于 “延迟加锁”, 加锁本身有一定开销, 能不加就不加, 尽量来避免不必要的加锁开销. 直到有人竞争才加锁. 类似于懒汉模式.
随着其他线程进入竞争, 偏向锁状态被消除, 进入轻量级锁状态(自适应的自旋锁).
synchronized
通过自旋锁的方式来实现轻量级锁.
一旦锁被占用, 另一个想要加锁的线程就会按照自旋的方式, 来反复查询当前所是否被释放.
自旋操作是一直让 CPU 空转, 比较浪费 CPU 资源.
因此此处的自旋不会一直持续进行, 而是达到一定的时间/重试次数, 就不再自旋了.
也就是所谓的 “自适应”
如果竞争进一步激烈, 自旋不能快速获取到锁状态, 就会膨胀为重量级锁.
编译器+JVM 会判断当前代码是否有必要加锁. 如果没必要, 就直接消除锁.
锁的粒度: 粗/细
如果加锁操作里面执行的代码越多, 锁的粒度越大; 反之越小
//1
synchronized (locker1) {
for (int i = 0; i < 100; i++) {
//
}
}
//2
for (int i = 0; i < 100; i++) {
synchronized (locker1) {
//
}
}
代码1中, 加锁操作里面执行的代码较多, 锁的粒度大
代码2中, 加锁操作里面执行的代码少, 锁的粒度小
实际开发过程中, 使用细粒度锁, 是期望释放锁的时候其他线程能使用锁.
但是实际上可能并没有其他线程来抢占这个锁. 这种情况 JVM 就会自动把锁粗化, 避免频繁申请释放锁.
理解锁粗化
给下属交代工作任务:
方式一:
打电话, 交代任务1, 挂电话.
打电话, 交代任务2, 挂电话.
打电话, 交代任务3, 挂电话.
方式二:
打电话, 交代任务1, 任务2, 任务3, 挂电话.
显然, 方式二是更高效的方案.