synchronized特点, 加锁过程, 优化操作

文章目录

  • 1. 基本特点
  • 2. 加锁过程
    • 2.1 偏向锁
    • 2.2 轻量级锁
    • 2.3 重量级锁
  • 3. 其他的优化操作
    • 3.1 锁消除
    • 3.2 锁粗化

1. 基本特点

  1. 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁.(自适应)
  2. 开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁.(自适应)
  3. 实现轻量级锁的时候大概率用到的自旋锁策略
  4. 是一种不公平锁
  5. 是一种可重入锁
  6. 不是读写锁

2. 加锁过程

代码中写了一个synchronized之后, 可能会产生一系列的"自适应过程", 叫锁升级, 也称为锁膨胀.

过程: 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁

2.1 偏向锁

第一个尝试加锁的线程, 优先进入偏向锁状态.

这里不是真的加锁, 只是给锁对象头中做一个 “偏向锁的标记”, 记录这个锁属于哪个线程.

如果没有别的线程竞争锁, 那就不会加锁了(避免了加锁解锁的开销);

如果后续有其他线程来竞争该锁(刚才已经在锁对象中记录了当前锁属于哪个线程了, 很容易识别当前申请锁的线程是不是之前记录的线程), 那就取消原来的偏向锁状态, 进入一般的轻量级锁状态.

偏向锁本质上相当于 “延迟加锁”, 加锁本身有一定开销, 能不加就不加, 尽量来避免不必要的加锁开销. 直到有人竞争才加锁. 类似于懒汉模式.

2.2 轻量级锁

随着其他线程进入竞争, 偏向锁状态被消除, 进入轻量级锁状态(自适应的自旋锁).

synchronized通过自旋锁的方式来实现轻量级锁.

一旦锁被占用, 另一个想要加锁的线程就会按照自旋的方式, 来反复查询当前所是否被释放.

自旋操作是一直让 CPU 空转, 比较浪费 CPU 资源.

因此此处的自旋不会一直持续进行, 而是达到一定的时间/重试次数, 就不再自旋了.

也就是所谓的 “自适应”

2.3 重量级锁

如果竞争进一步激烈, 自旋不能快速获取到锁状态, 就会膨胀为重量级锁.

  • 执行加锁操作, 先进入内核态.
  • 在内核态判定当前锁是否已经被占用
  • 如果该锁没有占用, 则加锁成功, 并切换回用户态.
  • 如果该锁被占用, 则加锁失败. 此时线程进入锁的等待队列, 挂起. 等待被操作系统唤醒.
  • 经历了一系列的沧海桑田, 这个锁被其他线程释放了, 操作系统也想起了这个挂起的线程, 于是唤醒这个线程, 尝试重新获取锁.

3. 其他的优化操作

3.1 锁消除

编译器+JVM 会判断当前代码是否有必要加锁. 如果没必要, 就直接消除锁.

3.2 锁粗化

锁的粒度: 粗/细

如果加锁操作里面执行的代码越多, 锁的粒度越大; 反之越小

//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, 挂电话.

显然, 方式二是更高效的方案.

你可能感兴趣的:(Javaee,开发语言,java)