synchronized

一、synchronized 的特性

synchronized 是 Java 语言内置的关键字,用于实现线程同步,保障多线程环境下对共享资源的安全访问。它是 Java 最基础、最常用的线程安全机制之一。synchronized 是一个可重入、非公平、互斥的锁。它通过偏向锁优化单线程访问,通过轻量级锁/自旋优化多线程交替访问,最后在竞争激烈时升级为重量级锁。同时,它天然具备原子性、可见性有序性,是保障线程安全的“三驾马车”。

(一)synchronized 的核心行为特性

  • 互斥性 (Mutual Exclusion):这是 synchronized 最根本的特性。它保证了同一时刻,只有一个线程能够获取到锁并执行临界区代码。当一个线程获取到锁后,其他试图获取同一把锁的线程会被阻塞,直到持有锁的线程释放锁。这确保了对共享资源的串行访问,解决了线程安全问题。

  • 可重入性 (Reentrant):synchronized 是可重入锁。这意味着同一个线程在已经持有锁的情况下,可以再次成功获取该锁而不会造成死锁。JVM 会记录锁的持有线程 ID 和一个计数器。当线程再次进入时,计数器递增;每次退出同步块时,计数器递减。只有当计数器归零时,锁才会真正被释放,其他线程才能获取。避免了“自己把自己锁死”的情况,也支持在 synchronized 方法中调用其他 synchronized 方法。

  • 内存可见性 (Visibility):synchronized 能够保证在释放锁之前,线程对共享变量的修改会刷新回主内存;而在获取锁之后,线程会从主内存重新读取最新的变量副本。这保证了多个线程之间共享变量的可见性,即一个线程修改了数据,其他线程能立即看到。

(二)锁机制与优化特性

  • 锁升级机制 (Lock Escalation):synchronized 的锁状态会随着竞争激烈程度动态升级,且升级过程不可逆
  1. 无锁状态

  2. 偏向锁 (Biased Locking): 初始状态。如果锁对象只有一个线程访问,JVM 会将锁“偏向”该线程,线程再次进入时不需要进行任何同步操作(CAS),直接通过检查对象头中的线程 ID 即可。

  3. 轻量级锁 (Lightweight Locking): 当有多个线程交替访问(无竞争或竞争不激烈)时,JVM 使用 CAS 操作来尝试获取锁,避免了操作系统层面的线程阻塞和唤醒,提高了性能。

  4. 重量级锁 (Heavyweight Locking): 当竞争激烈(如线程自旋超过一定次数仍未获取到锁),锁会膨胀为重量级锁。此时依赖操作系统的互斥量(Mutex)来实现,线程会进入阻塞状态,需要操作系统内核进行调度,开销最大。

  • 自旋锁策略 (Spin Lock):在轻量级锁状态下,如果线程获取锁失败,它不会立即挂起(阻塞),而是会执行一段忙循环(自旋),看持有锁的线程是否很快就会释放。这是一种“以时间换空间”的策略,避免了线程切换的开销。

  • 非公平锁:synchronized 是非公平锁。这意味着当锁被释放时,等待时间最长的线程并不一定能优先获取到锁。新来的线程和队列中等待的线程有同等的概率竞争到锁,这可能导致某些线程长时间无法获取锁(饥饿),但整体吞吐量通常比公平锁更高。

二、synchronized的使用

synchronized 的本质是加锁,但锁的对象会根据使用位置的不同而变化。

(一)修饰实例方法 (对象锁):当你用 synchronized 修饰一个普通方法时,锁的是当前实例对象(this。适用场景: 保护该实例对象的成员变量,防止多个线程同时修改同一个对象的状态。

public class Counter {
    private int count = 0;

    // 锁住的是调用这个方法的实例对象 (this)
    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

(二)修饰静态方法 (类锁):当你用 synchronized 修饰一个静态方法时,锁的是该类的 Class 对象(类名.class。适用场景为保护静态变量(类变量),因为静态变量属于类,不属于某个具体实例。

public class Counter {
    private static int totalCount = 0;

    // 锁住的是 Counter.class 对象
    public static synchronized void incrementTotal() {
        totalCount++;
    }
}

(三)修饰代码块(同步代码块):这是粒度最细、最灵活的用法。你可以指定任意对象作为锁。适用场景: 只需要同步方法中的一部分代码(临界区),以减少锁的持有时间,提高并发性能。

public class Counter {
    private int count = 0;
    private final Object lock = new Object(); // 专用锁对象

    public void increment() {
        // 只锁住必要的代码块,而不是整个方法
        synchronized (lock) {
            count++;
        }
        // 这里可以执行不需要同步的耗时操作,不阻塞其他线程
    }
}

三、synchronized的锁机制

Java 中 synchronized 的锁机制是一个非常精妙的设计,它并不是一成不变的“重量级”锁,而是随着竞争激烈程度动态升级的。

(一)、核心组件

synchronized 的实现依赖于 JVM 对 Java 对象的管理和操作系统底层的互斥机制。

  • 对象头 (Object Header) 每个 Java 对象在内存中都有一个对象头,其中的 Mark Word 是核心。它在不同的锁状态下,存储的内容是不一样的(比如存储哈希码、线程 ID、指向锁记录的指针等)。
  • Monitor (监视器/管程) 这是 synchronized 实现同步的基础。每个 Java 对象都天生关联一个 Monitor。
  1. Owner: 指向持有该锁的线程。
  2. Entry Set: 存放等待获取锁的线程队列。
  3. Wait Set: 存放调用了 wait() 方法的线程队列。

(二)锁升级机制

这是 synchronized 性能优化的关键。为了减少线程阻塞和唤醒的开销,JVM 会根据竞争情况,从低级别的锁向高级别的锁自动升级。这个过程是不可逆的(只能升级,不能降级)。

  • 1. 无锁状态 (No Lock)

  • 偏向锁 (Biased Locking) —— [优化单线程]
  1. 场景: 大多数情况下,锁不仅没有竞争,而且总是由同一个线程多次获取。

  2. 机制: JVM 会把该锁“偏向”于当前线程。它会在对象头的 Mark Word 中记录该线程的 ID。

  3. 优点: 下次同一个线程再进入同步块时,不需要进行任何同步操作(不需要 CAS 操作),直接执行即可。加锁解锁几乎没有额外开销。

  4. 触发升级: 当有第二个线程尝试获取锁时,偏向锁就会失效,升级为轻量级锁。

  • 轻量级锁 (Lightweight Locking) —— [优化多线程交替]
  1. 场景: 线程间存在竞争,但不是特别激烈,且持有锁的时间很短。

  2. 机制:线程在自己的栈帧中建立一个锁记录 (Lock Record)。通过 CAS 操作,尝试将对象头的 Mark Word 指向这个锁记录。如果成功,获取锁;如果失败(说明有竞争),线程不会立即阻塞,而是进入自旋(Spin)状态,循环尝试获取锁。

  3. 优点: 避免了线程阻塞和唤醒的开销(用户态与内核态切换)。

  4. 缺点: 如果自旋次数过多(默认 10 次)或者竞争加剧,会消耗 CPU 资源。

  •  重量级锁 (Heavyweight Locking) —— [终极手段]
  1. 场景: 多线程竞争激烈,或者锁被持有时间很长。

  2. 机制: 当自旋超过一定阈值,或者等待线程过多时,锁会膨胀为重量级锁。

  3. 原理: 此时依赖操作系统的互斥量 (Mutex) 来实现。对象头的 Mark Word 指向一个重量级的 Monitor 对象。

  4. 表现: 所有未获取到锁的线程都会被阻塞(挂起),进入内核调度状态,不再消耗 CPU。只有当持有锁的线程释放锁后,操作系统才会唤醒阻塞队列中的一个线程。

  5. 缺点: 线程阻塞和唤醒涉及上下文切换,开销很大。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值