Java Synchronized 关键字

Synchronized 是什么?

sychronied是Java中并发编程的重要关键字之一。在并发编程中synchronized一直是解决线程安全问题,它可以保证原子性,可见性,以及有序性。

  • 原子性 :保证这段代码不会被并发打断
    synchronized 通过 monitorenter / monitorexit 实现互斥。同一时刻只有一个线程能进入同一把锁保护的临界区,因此临界区内的复合操作整体对外表现为不可分割。同一个锁,同一时刻只能被一个线程持有。因此临界区代码不会被并发打断。
  • 可见性 :释放锁前的修改,对之后获取同一把锁的线程可见
    JMM 中有非常重要的 Happens-Before 规则:: 对一个锁的解锁 happens-before 于后续对同一个锁的加锁。
    synchronized 在释放锁时,会保证此前的写操作对之后获取同一锁的线程可见;线程重新获得锁后,能够看到前一个线程释放锁前的修改。
  • 有序性 : 禁止重排序破坏同步语义
    CPU 和编译器为了优化性能,可能会进行指令重排序 。在不影响单线程执行结果的情况下,底层有可能调整某些指令执行顺序。
    允许不影响程序语义的优化和重排序,但不能发生破坏 synchronized 同步语义、Happens-Before 关系的重排序。
    即: synchronized 会建立明确的同步边界,编译器和 CPU 不能进行那种破坏锁的 Happens-Before 关系和程序同步语义的重排序。
text 复制代码
线程A临界区操作   // 先于 线程A unlock
        ↓
   unlock(lock)
        ↓
 Happens-Before
        ↓
    lock(lock)
        ↓
线程B临界区操作 // 晚于 线程B lock

synchronized 可以保证原子性、可见性和有序性。原子性来自锁的互斥机制,同一把锁同一时刻只能被一个线程持有,因此同步代码块不会被多个线程同时执行。可见性来自 JMM 的 Happens-Before 规则,一个线程对锁的释放 Happens-Before 于另一个线程随后对同一把锁的获取,因此前一个线程的修改对后一个线程可见。有序性则是通过同步语义限制编译器和 CPU 的指令重排序,不能让重排序破坏锁建立的 Happens-Before 关系。


Synchronized 使用

synchronized 可以修饰方法,也能够使用 synchronized(obj){} 定义同步代码块。

  • 修饰方法
  1. 修饰实例方法
java 复制代码
public synchronized void test() { 
	// ... 
}

锁住的是当前实例对象 this。等价于

java 复制代码
public void test() { synchronized (this) { 
	// ... 
	} 
}
  1. 修饰静态方法
java 复制代码
public static synchronized void test() { 
	// ... 
}

锁住的是当前类对应的 Class 对象,等价于:

java 复制代码
public static void test() {
    synchronized (Test.class) {
        // ...
    }
}

由于一个类通常只有一个对应的 Class 对象,因此无论通过哪个实例调用,竞争的都是同一把类锁。

  1. 修饰代码块
java 复制代码
synchronized (obj) {
    // ...
}

锁住的是指定的对象 obj。

线程进入同步代码块之前,必须先获得 obj 对应的锁;退出代码块后释放锁。


三种方式对比

写法 加锁对象
synchronized 实例方法 当前实例 this
static synchronized 静态方法 当前类的 Class 对象,如 Test.class
synchronized(obj) 指定的 obj 对象

使用sychronied修饰普通方法和静态方法,其实也等价于synchronized(this){}synchronized(class){}


Synchronized 锁升级机制

在 JDK 6 之前,Synchronized 的实现是 重量级锁。

synchronized 的锁升级是 JDK 6 之后对 synchronized 的优化,核心目的是:在无竞争或轻竞争时,尽量避免直接使用重量级锁,减少用户态和内核态切换的开销。

java 对象在 虚拟机内存中的结构:

text 复制代码
Java 对象 = 对象头 + 实例数据 + 对齐填充

对象头 = Mark Word + Klass Pointer + Array Length(仅数组对象有)

Mark Word 存储对象自身的运行时数据,32位 四字节,64位 八字节;
Klass Pointer 指向方法区中该对象的类元数据,32 位 四字节,64位 开启压缩指针(默认)四字节 否则,八字节;
Array Length 数组对象的长度,占 4 字节。

锁状态主要记录在 Java 对象头的 Mark Word 中,升级过程通常是:

无锁 → 偏向锁 → 轻量级锁 → 重量级锁

一般认为是不可逆的,但偏向锁可以被撤销。JDK 15 之后偏向锁默认禁用,新版本中逐步移除;如果面试官问 JDK 8,则默认开启。

以32位 JVM 为例,Mark Word 如下:

锁升级流程
text 复制代码
                第一次进入 synchronized
                         ↓
                        无锁
                         ↓
                       偏向锁
                         ↓
                   出现其他线程竞争
                         ↓
                      轻量级锁
                         ↓
                   竞争进一步加剧
                         ↓
                      重量级锁

1. 无锁

对象刚创建时是无锁状态。

Mark Word 中存 hashCode、GC 年龄、锁标志位等。

以 32 位为例:锁标志位是 01,偏向位是 0。

当线程第一次 进入 synchronized, 会将锁偏向该线程,锁成为偏向锁。

2. 偏向锁

首先之所以会引入偏向锁是因为:大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁,减少不必要的操作。

在程序进入同步代码块时,会访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,若为偏向锁状态,则查看偏向锁状态下线程ID是否指向当前线程 。如果是则直接执行同步代码。

但是mark word中记录的线程ID如果不是当前线程,则通过CAS比较与交换尝试修改对象头获得锁。CAS操作成功则可以直接执行同步代码,否则表示有其他线程竞争,此时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁 ,然后被阻塞的线程继续往下执行同步代码。

**适用场景:**只有一个线程反复进入同步块。

3. 轻量级锁

轻量级锁状态下,代码进入同步块时,如果同步对象锁状态为无锁状态, 虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝 ,接着虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针 。这个操作如果成功则代表获取到了锁,但是如果失败,则会检查对象Mark Word是不是指向当前线程栈帧中的锁记录,如果是,则说明本身当前线程就拥有此对象的锁,就可以直接执行同步代码。否则说明锁对象被其他线程获取,当前线程是竞争者,那么当前线程会自旋等待锁,也就是不断重试,当重试一定次数后,总不能一直重试下去吧,太耗CPU了。所以这时候就要升级为重量级锁。

**适用场景:**少量线程交替执行,竞争不激烈。

4. 重量级锁

重量级锁就是通过对象监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。主要是,当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。

**适用场景:**多线程激烈竞争。

相关推荐
lhldsg43 分钟前
折扣卡CPS小程序定制全流程实战指南:从需求分析到上线部署
java·前端·小程序
她的男孩1 小时前
多租户隔离怎么落地?拆完这1600行Starter源码,我把5个坑全踩明白了
java·后端·架构
ly76891 小时前
Spring 事件机制在微服务中实现最终一致性的设计
java·spring·微服务·异步处理·最终一致性·spring事件·事务事件
wuminyu1 小时前
Kafka利用sendfile与Page Cache实现高性能传输剖析
java·linux·c语言·jvm·c++
我命由我123451 小时前
Android 开发问题:android.permission.CAMERA...duplicated with element declared at
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
Bs_MoneyMagnet1 小时前
基于springboot+vue的医院陪诊服务预约平台的设计与实现 源码+文档
java·vue.js·spring boot·后端·spring
SimonKing1 小时前
一个Docker命令,40万首古诗词API开箱即用
java·后端·程序员
青春易逝丶1 小时前
SpringDataJPA
java·spring·hibernate
某不知名網友1 小时前
C++ 深浅拷贝:从默认拷贝到 Rule of Five
java·开发语言