当面试官问到你synchronized的原理时,别再一头雾水了

一、synchronized的特性

(1)互斥

synchronized 会起到互斥效果,某个线程执行到某个对象的 synchronized 中时,其他线程如果也执行到同一个对象 synchronized 就会阻塞等待synchronized 通过互斥达到原子性。

  1. 进入 synchronized 修饰的代码块,相当于 加锁
  2. 退出 synchronized 修饰的代码块,相当于 解锁在这里插入图片描述
    synchronized用的锁是存在Java对象头里的,它的底层是使用操作系统的mutex lock实现的。
    一个线程上了锁之后,其他线程就只能等待这个线程释放锁(这个过程不可中断,只能一直等待,直到加锁为止),上一个线程解锁之后, 下一个线程并不是立即就能获取到锁,而是要靠操作系统来 “唤醒”, 这也就是操作系统线程调度的一部分工作。
    假设有 A B C 三个线程,线程 A 先获取到锁,然后 B 尝试获取锁, 然后 C 再尝试获取锁,此时 B 和 C 都在阻塞队列中排队等待。但是当 A 释放锁之后,虽然 B 比 C 先来的, 但是 B 不一定就能获取到锁,而是和 C 重新竞争,并不遵守先来后到的规则。

(2)刷新内存

synchronized 的工作过程:

  1. 获得互斥锁
  2. 从主内存拷贝变量的最新副本到工作的内存
  3. 执行代码
  4. 将更改后的共享变量的值刷新到主内存
  5. 释放互斥锁

通过这个过程, synchronized保证了内存可见性。

(3)可重入

synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题。
什么是把自己锁死?
即一个线程没有释放锁, 然后又尝试再次加锁。
例如:
在这里插入图片描述
按照之前对于锁的设定,第二次加锁的时候,就会阻塞等待,直到第一次的锁被释放,才能获取到第二个锁,但是释放第一个锁也是由该线程来完成,结果这个线程已经躺平了,啥都不想干了,也就无法进行解锁操作,这时候就会死锁。这样的锁成为不可重入锁,synchronized是可重入锁,不包含这样的问题。

在可重入锁的内部,包含了 “线程持有者” 和 “计数器” 两个信息:

  1. 如果某个线程加锁的时候,发现锁已经被人占用,但是恰好占用的正是自己,那么仍然可以继续获取到锁,并让计数器自增。
  2. 解锁的时候计数器递减为 0 的时候,才真正释放锁。

(4)非公平锁和不可中断

这也是synchronized的特性。
非公平锁即上述举到的例子:假设有 A B C 三个线程,线程 A 先获取到锁,然后 B 尝试获取锁, 然后 C 再尝试获取锁,此时 B 和 C 都在阻塞队列中排队等待。但是当 A 释放锁之后,虽然 B 比 C 先来的, 但是 B 不一定就能获取到锁,而是和 C 重新竞争,并不遵守先来后到的规则
不可中断即:一个线程上了锁之后,其他线程就只能等待这个线程释放锁(这个过程不可中断,只能一直等待,直到加锁为止)。

二、synchronized的使用

(1)直接修饰普通方法

锁的是对象(单个对象内部加锁):

public class SynchronizedDemo {
    public synchronized void methond() {
   }
}

(2)修饰静态方法

锁的是类的所有对象:

public class SynchronizedDemo {
    public synchronized static void method() {
   }
}

(3)修饰代码块

明确指定锁哪个对象:
锁当前对象:

public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
       }
   }
}

锁类对象:

public class SynchronizedDemo {
    public void method() {
        synchronized (SynchronizedDemo.class) {
       }
   }
}

只有两个线程竞争同一把锁,才会有锁冲突,才会产生阻塞等待。

三、synchronized的锁机制

(1)加锁过程(锁升级)

JVM将 synchronized 锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。会根据情况,进行依次升级。
如图:
在这里插入图片描述
偏向锁:
第一个尝试加锁的线程,优先进入偏向锁状态。
偏向锁不是真的 “加锁”,只是给对象头中做一个 “偏向锁的标记”,记录这个锁属于哪个线程。如果后续没有其他线程来竞争该锁,那么就不用进行其他同步操作了(避免了加锁解锁的开销)如果后续有其他线程来竞争该锁(刚才已经在锁对象中记录了当前锁属于哪个线程了,很容易识别当前申请锁的线程是不是之前记录的线程),那就取消原来的偏向锁状态,进入一般的轻量级锁状态。偏向锁本质上相当于 “延迟加锁”,能不加锁就不加锁,尽量来避免不必要的加锁开销。但是该做的标记还是得做的,否则无法区分何时需要真正加锁。

轻量级锁:

随着其他线程进入竞争,偏向锁状态被消除,进入轻量级锁状态(自适应的自旋锁)。
此处的轻量级锁就是通过 CAS 来实现。

  1. 通过 CAS 检查并更新一块内存 (比如 null => 该线程引用)
  2. 如果更新成功,则认为加锁成功
  3. 如果更新失败,则认为锁被占用,继续自旋式的等待(并不放弃 CPU)

PS:自旋操作是一直让 CPU 空转,比较浪费 CPU 资源,因此此处的自旋不会一直持续进行,而是达到一定的时间/重试次数, 就不再自旋了,也就是所谓的 "自适应“。

重量级锁:
如果竞争进一步激烈,自旋不能快速获取到锁状态,就会膨胀为重量级锁。
此处的重量级锁就是指用到内核提供的 mutex锁

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

(2)优化操作

锁消除:

编译器+JVM 会判断锁是否可消除,如果可以,就直接消除。
例如:
有些应用程序的代码中,用到了synchronized,但其实没有在多线程环境下。

StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
sb.append("d");

此时每个 append 的调用都会涉及加锁和解锁,但如果只是在单线程中执行这个代码,那么这些加
锁解锁操作是没有必要的,白白浪费了一些资源开销。

锁粗化:

一段逻辑中如果出现多次加锁解锁,编译器 + JVM 会自动进行锁的粗化。
锁的粒度: 粗和细
在这里插入图片描述
实际开发过程中,使用细粒度锁,是期望释放锁的时候其他线程能使用锁。但是实际上可能并没有其他线程来抢占这个锁,这种情况 JVM 就会自动把锁粗化,避免频繁申请释放锁。

四、synchronized的实现

(1)底层汇编

在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

木木是木木

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值