一次性搞懂系列:synchronized

简介

Java 中内置关键字,实现线程同步,支持可重入的独占锁。

特性

  • 原子性:确保线程互斥访问同步代码。
  • 可见性:保证共享变量修改及时可见。lock操作会清空工作空间变量值;执行unlock前,变量同步主内存。
  • 有序性:synchronized内代码和外部代码禁止指令重排,内部代码不会禁止重排。
  • 独占锁:一次只能被一个线程持有,其他线程阻塞。
  • 非公平锁:线程直接尝试获取锁,不判断是有有阻塞线程。
  • 可重入锁
三种使用方式
  • 代码块
Object obj = new Object();
public void test(){
    synchronized (obj){
        // ....
    }
}
  • 方法
public synchronized void test(){
}
  • 静态方法
public synchronized static void test(){
}

这三种有什么区别,到底锁的是什么?

对象

这不跟没说一样……

he~tui吐痰表情包_抖音he~tui吐痰图片下载_游戏吧

本质上就是锁的是对象,代码块是obj对象,方法其实就是实例对象,就是this,这两种只有是同一个实例才会存在锁竞争;静态方法就是类对象,只要有一个线程获取到锁,其他线程就无法访问。

实现原理

方法和对象加锁编译后字节码

image-20240516134401380

image-20240516134502951

底层原理是基于JVM的指令和对象的监视器(monitor)来实现的。

线程访问被synchronized修改时方法或代码块,先获取所属对象监视器,获取成功,执行代码块,监视器计数+1,如果失败,就会阻塞,直到监视器被释放。

synchronized修饰方法时,ACC_SYNCHRONIZED标识方法是同步的;

synchronized修饰代码块时,monitorenter和monitorexit两个指令用来进入和退出监视器;


具体实现

在HotSpot虚拟机中,Java 对象在内存布局中大体分:对象头、实例数据和对其填充

而普通对象头由Mark Word、Klass point;Mark Word占8字节,对象头布局如下:

OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)

后三位表示锁 第一位:无锁:0,偏向锁:1 后两位:无锁:01,偏向锁:01,轻量级锁:00,重量级锁:10,gc标记:11

  • 无锁(偏向锁:0,锁标志位:01)

  • 偏向锁(偏向锁:1,锁标志位:01,线程ID)

    1. 偏向锁标志位是否是1,锁标志位是否为01,确认偏向状态
    2. 如果为可偏向状态,是否为当前线程,如果是,进入同步操作,否则进入一步
    3. CAS获取偏向锁失败,标识资源被占。当到达安全点时,偏向锁升级为轻量级锁,然后被阻塞在安全点继续执行同步代码
    4. CAS获取偏向锁失败,标识资源被占。当到达安全点时,偏向锁升级为轻量级锁,然后被阻塞在安全点继续执行同步代码
  • 轻量级锁(锁标志位:00)

    1. 代码进入同步块,如果对象锁为偏向状态通过CAS抢占,锁标志位为00
    2. 抢到,进入同步
    3. 未抢到,循环执行,循环一定阶段,升级重量级锁
  • 重量级锁(锁标志位:10)

    1. 分配ObjectMonitor对象,锁标志位设为10,mark word存储指向ObjectMonitor
    2. ObjectMonitor有两个队列,用来保存ObjectWaiter对象列表,多个线程访问同一段代码,首先进入EntryList
    3. 通过CAS尝试将Monitor owner修改为当前线程,count+1,发现owner是当前线程recursions+1,CAS失败,进入EntryList中
    4. 当获取锁线程调用wait方法,owner设置null,count-1,recursions-1,当前线程加入waitSet,等待被唤醒
    5. 线程执行完代码块时,释放锁,count-1,recursions-1,recursions为0,说明线程释放锁
做了哪些优化
  • 锁膨胀

    锁膨胀:无锁 到 偏向锁 ,再到轻量级锁,最后变成重量级锁。也叫锁升级(不可降级

    JDK1.6之前,synchronized是重量级锁,释放、获取锁都会从用户态到内核态转换,效率低,引入锁膨胀,偏向锁、轻量级锁在并发操作不用用户态、内核态转换,提交性能。

  • 锁粗化

    将多个连续的加锁、解锁连接在一起变成范围更大的锁。

  • 锁消除

    某些情况下,虚拟机检测不到某段代码存在竞争的可能,将代码同步消除掉,从而提交程序性能。

  • 自旋锁

    通过自身循环,尝试获取锁;优点:避免一些线程挂起、恢复操作,如果循环次数太多,浪费一定资源,需要设置阈值。

面试知识点
  • 什么是synchronized

  • 什么是可重入锁,synchronized如何实现的

    当线程持有锁,当前线程可以多次进入被锁定的代码块或方法而不会被阻塞;

    占有锁后对象头中会有线程ID,可重入锁,每次进入+1,退出-1,直至状态为0,释放锁

  • 什么是锁升级

    同锁膨胀

  • synchronized是非公平还是公平的

    非公平

  • synchronized如何保证线程安全的

    • 原子性
    • 有序性
    • 可见性
  • 如何提升synchronized性能

    本质上减少锁竞争

    尽量保证锁处于轻量级锁或偏向锁

    减小锁粒度

    减小锁持有时间

  • synchronized和ReentrantLock区别

    相同

    都是可重入锁,可实现线程同步

    区别

    1. synchronized是关键字,依赖jvm;ReentrantLock是通过AQS编程实现
    2. ReentrantLock更加灵活,功能丰富,等待可中断、可实现公平锁

这是一个从 https://juejin.cn/post/7369248993106280500 下的原始话题分离的讨论话题