在前一篇内容中,我们从 CPU 缓存原理讲清了可见性、有序性问题,也知道volatile能解决这两个问题,但它有个致命短板 —— 无法保证原子性。而在并发编程中,原子性是解决多线程操作同一变量的核心诉求,这时候就需要 synchronized 登场。

一、synchronized 的 3 个使用误区

误区 1:分开加锁 = 没加锁

原则:需要保证原子性的完整操作,必须全部包裹在同一把锁内,操作没完成不释放锁。如果把一个原子操作拆成多段、分开加锁,中间的无锁区间会让并发问题卷土重来。

错误示范:
public class WrongSyncDemo {
    private int count = 0;

    // 错误:把count++拆成"读→计算→写"三段,仅给读和写单独加锁
    public void wrongAdd() {
        // 第一步:加锁读
        synchronized (this) {
            count = count; // 仅演示读操作
        }
        // 无锁区间:其他线程可修改count,产生脏数据
        int temp = count + 1;
        
        // 第二步:加锁写
        synchronized (this) {
            count = temp;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        WrongSyncDemo demo = new WrongSyncDemo();
        // 两个线程各执行10000次加操作
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) demo.wrongAdd();
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) demo.wrongAdd();
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        // 结果远小于20000,并发问题依然存在
        System.out.println("最终count值:" + demo.count); 
    }
}
完整操作加锁
public class CorrectSyncDemo {
    private int count = 0;

    // 正确:把"读→计算→写"完整操作包裹在同一把锁内
    public void correctAdd() {
        synchronized (this) { // 加锁后,直到代码块结束才释放
            count++; // 三步原子完成,无中间间隙
        }
    }

    public static void main(String[] args) throws InterruptedException {
        CorrectSyncDemo demo = new CorrectSyncDemo();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) demo.correctAdd();
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) demo.correctAdd();
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        // 结果精准等于20000,原子性得到保证
        System.out.println("最终count值:" + demo.count); 
    }
}

误区 2:误以为非静态同步方法只锁 “当前方法”

非静态synchronized方法的锁是当前实例对象(this),而非某个方法。

  • 对同一个实例:只要有一个非静态同步方法被锁住,该实例的所有非静态同步方法都会被阻塞(“锁住一个,锁住全部”)
  • 对不同实例:每个实例有独立的锁,互不影响
代码验证
public class InstanceLockDemo {
    // 非静态同步方法1
    public synchronized void m1() {
        System.out.println(Thread.currentThread().getName() + " 进入m1");
        try {
            Thread.sleep(3000); // 模拟长时间操作
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " 退出m1");
    }

    // 非静态同步方法2
    public synchronized void m2() {
        System.out.println(Thread.currentThread().getName() + " 进入m2");
    }

    // 非同步方法(不受锁影响)
    public void m3() {
        System.out.println(Thread.currentThread().getName() + " 进入m3");
    }

    public static void main(String[] args) {
        InstanceLockDemo demo = new InstanceLockDemo();
        // 线程1调用m1,占用demo实例锁
        new Thread(() -> demo.m1(), "线程1").start();
        // 线程2调用m2,需等待线程1释放demo锁
        new Thread(() -> demo.m2(), "线程2").start();
        // 线程3调用m3,不受锁影响,立即执行
        new Thread(() -> demo.m3(), "线程3").start();
    }
}

输出结果

线程1 进入m1
线程3 进入m3
线程1 退出m1
线程2 进入m2

误区 3:误以为静态同步方法的锁和非静态同步方法共用

静态synchronized方法的锁是类的 Class 对象(存储在 JVM 方法区,整个类只有 1 份),而非实例对象。

  • 静态同步方法的 “类锁” 和非静态同步方法的 “实例锁” 完全独立,互不干扰
  • 对同一个类:只要有一个静态同步方法被锁住,该类的所有静态同步方法都会被阻塞
  • 不加锁的静态方法、非静态方法,不受类锁影响
代码验证
public class ClassLockDemo {
    // 静态同步方法1
    public static synchronized void staticM1() {
        System.out.println(Thread.currentThread().getName() + " 进入staticM1");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " 退出staticM1");
    }

    // 静态同步方法2
    public static synchronized void staticM2() {
        System.out.println(Thread.currentThread().getName() + " 进入staticM2");
    }

    // 非静态同步方法(锁为实例对象,与类锁独立)
    public synchronized void instanceM() {
        System.out.println(Thread.currentThread().getName() + " 进入instanceM");
    }

    public static void main(String[] args) {
        ClassLockDemo demo = new ClassLockDemo();
        // 线程1调用静态同步方法,占用Class锁
        new Thread(() -> ClassLockDemo.staticM1(), "线程1").start();
        // 线程2调用静态同步方法,需等待Class锁释放
        new Thread(() -> ClassLockDemo.staticM2(), "线程2").start();
        // 线程3调用非静态同步方法,用的是实例锁,立即执行
        new Thread(() -> demo.instanceM(), "线程3").start();
    }
}

执行结果:

线程1 进入staticM1
线程3 进入instanceM
线程1 退出staticM1
线程2 进入staticM2

二、synchronized 的锁升级机制

很多人觉得synchronized是 “重量级锁”,但 Java 1.6 后 JVM 对其做了优化 —— 引入锁升级,让大部分场景下的synchronized性能接近无锁。

锁升级的逻辑:从偏向锁→轻量级锁→重量级锁,根据竞争激烈程度逐步升级,避免一开始就使用低效的重量级锁。

1. 锁的存储位置:对象头(Mark Word)

JVM 中每个对象的头部(Mark Word)会存储锁的状态、持有线程 ID 等信息,锁升级的本质就是修改对象头的标记。

2. 锁升级流程

锁类型 适用场景 核心逻辑
偏向锁 无竞争 / 单线程重复加锁 给对象头标记 “偏向线程 ID”,后续该线程加锁无需 CAS,直接获取(几乎无开销)
轻量级锁 轻度竞争(线程交替加锁) 无竞争线程尝试用 CAS 修改对象头获取锁,失败则自旋等待(避免内核态切换)
重量级锁 重度竞争(线程同时加锁) 自旋多次失败后,升级为重量级锁,依赖操作系统互斥量(会触发上下文切换)

3. 结论

  • 锁升级是单向的(偏向锁→轻量级锁→重量级锁),不会降级
  • 大部分业务场景中,synchronized停留在偏向锁 / 轻量级锁阶段,性能并不差
  • 只有高并发、激烈竞争的场景下,才会升级为重量级锁,产生明显性能开销

三、ynchronized 该怎么用?

  1. 锁粒度要精准:只锁需要保证原子性的代码块,而非整个方法(比如转账业务,只锁 “扣减 A 账户 + 增加 B 账户” 的代码块,而非整个转账方法)
  2. 锁对象要选对
    • 实例相关的共享变量:用实例锁(非静态同步方法 /synchronized(this)
    • 类相关的共享变量:用类锁(静态同步方法 /synchronized(XXX.class)
  3. 避免锁竞争:尽量让锁停留在偏向锁 / 轻量级锁阶段(比如减少多线程同时加锁、避免锁持有时间过长)
  4. 不要滥用锁:如果只是保证可见性,用volatile即可;只有需要保证原子性时,再用synchronized

总结

  1. synchronized的核心是保证原子性,使用时必须把完整操作包裹在同一把锁内,分开加锁等于没加锁
  2. 非静态同步方法锁的是实例对象,静态同步方法锁的是 Class 对象,两类锁相互独立
  3. 实际需精准控制锁粒度和锁对象,根据竞争程度选择合适的同步方案
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐