Java 同步关键字 synchronized用法 、场景及避坑指南

 更新时间:2025年12月22日 12:02:27   作者:heartbeat..  
synchronized 是 Java 最基础、最常用的线程同步方式,优点是 简单易用、无需手动释放锁、JVM 自动优化,适合解决大多数线程安全问题,本文给大家介绍Java 同步关键字 synchronized用法 + 场景 + 避坑指南,感兴趣的朋友跟随小编一起看看吧

Java 同步关键字 synchronized:用法 + 场景 + 避坑指南

一、介绍

在 Java 中,synchronized内置的线程同步机制,用于解决多线程并发访问共享资源时的线程安全问题(如竞态条件、数据不一致)。其核心原理是 通过 “锁” 控制多个线程对共享资源的互斥访问,确保同一时刻只有一个线程能执行临界区代码。

二、主要特点

  1. 互斥性:同一时刻,只有一个线程能持有锁并执行临界区代码(其他线程需阻塞等待锁释放)。
  2. 可见性:锁释放时,线程对共享变量的修改会被 “刷新” 到主内存;其他线程获取锁时,会从主内存重新读取变量(避免缓存一致性问题)。
  3. 有序性:禁止指令重排序(临界区代码在多线程视角下按顺序执行)。

举个例子:

synchronized 就像一个 “共享资源的专属门卫”

  • 它守着一个 “资源入口”(临界区);
  • 每个想用资源的 “线程”,都得先跟门卫要 “通行证”(获取锁);
  • 门卫只给一个人发通行证,其他人只能在门口排队(阻塞);
  • 拿到通行证的人用完资源后,必须把通行证还给门卫(释放锁),下一个排队的人才能拿到;
  • 门卫还认人(可重入性):同一个人再要通行证,直接给,不用排队;
  • 门卫还能灵活守 “小门”(代码块)或 “大门”(整个方法):守小门效率高,守大门简单但容易堵。

synchronized 的核心作用 ——让多线程 “排队使用共享资源”,避免混乱和冲突

三、使用场景(3 种形式)

synchronized 可修饰 方法代码块,本质是对 “锁对象” 加锁,锁的粒度决定同步范围。

1. 修饰实例方法(对象锁)

  • 锁对象:当前 实例对象(this)
  • 效果:同一实例的多个线程,竞争同一把锁;不同实例的线程互不影响(各自持有自己的锁)。
public class SynchronizedDemo {
    // 实例方法加锁:锁是当前对象(this)
    public synchronized void instanceMethod() {
        // 临界区:共享资源操作(如修改实例变量)
        System.out.println(Thread.currentThread().getName() + " 执行实例方法");
        try { Thread.sleep(1000); } catch (InterruptedException e) {}
    }
    public static void main(String[] args) {
        SynchronizedDemo demo = new SynchronizedDemo();
        // 两个线程竞争 demo 实例的锁,串行执行
        new Thread(demo::instanceMethod, "线程1").start();
        new Thread(demo::instanceMethod, "线程2").start(); // 等待线程1释放锁
    }
}

2. 修饰静态方法(类锁)

  • 锁对象:当前类的 Class 对象(类的唯一全局锁)
  • 效果:所有该类的实例(无论多少个对象)共享同一把锁,多线程竞争类锁时串行执行。
public class SynchronizedDemo {
    // 静态方法加锁:锁是 SynchronizedDemo.class
    public static synchronized void staticMethod() {
        System.out.println(Thread.currentThread().getName() + " 执行静态方法");
        try { Thread.sleep(1000); } catch (InterruptedException e) {}
    }
    public static void main(String[] args) {
        SynchronizedDemo demo1 = new SynchronizedDemo();
        SynchronizedDemo demo2 = new SynchronizedDemo();
        // 两个线程竞争同一把类锁(SynchronizedDemo.class),串行执行
        new Thread(demo1::staticMethod, "线程A").start();
        new Thread(demo2::staticMethod, "线程B").start(); // 等待线程A释放锁
    }
}

3. 修饰代码块(显式指定锁对象)

  • 锁对象:可自定义(任意 非 null 对象,如 this、Class 对象、自定义对象)。
  • 效果:仅对代码块内的逻辑加锁,粒度更细(推荐,减少锁竞争开销)。
public class SynchronizedDemo {
    private final Object lock = new Object(); // 自定义锁对象(推荐用 final,避免锁对象被修改)
    private int count = 0;
    public void syncBlock() {
        // 1. 自定义对象锁:锁定 lock 对象
        synchronized (lock) {
            count++;
            System.out.println(Thread.currentThread().getName() + ":count=" + count);
        }
        // 2. 实例锁(等价于修饰实例方法)
        synchronized (this) {
            // 临界区逻辑
        }
        // 3. 类锁(等价于修饰静态方法)
        synchronized (SynchronizedDemo.class) {
            // 临界区逻辑
        }
    }
    public static void main(String[] args) {
        SynchronizedDemo demo = new SynchronizedDemo();
        // 多线程竞争 lock 对象,count 自增线程安全
        for (int i = 0; i < 3; i++) {
            new Thread(demo::syncBlock, "线程" + i).start();
        }
    }
}

举个示例:

假设某电商平台有一款爆款商品,库存仅剩 10 件。同时有 100 个用户抢购(每个用户对应一个线程),每个用户抢购 1 件商品。要求:

  1. 库存不能为负数;
  2. 最终卖出的商品数量 = 初始库存(不能多卖,也不能少卖)。

如果不加 synchronized,会出现 “超卖”(比如库存 10,但卖出 12 件);加了 synchronized 后,能保证线程安全,精准扣减库存。

不加synchronized(线程不安全,会超卖)

public class StockDemo {
    // 商品初始库存:10件
    private int stock = 10;
    // 抢购方法(未加锁)
    public void buy() {
        // 1. 检查库存是否充足
        if (stock > 0) {
            // 模拟网络延迟(放大线程安全问题)
            try { Thread.sleep(10); } catch (InterruptedException e) {}
            // 2. 库存扣减
            stock--;
            // 3. 打印抢购结果
            System.out.println(Thread.currentThread().getName() + " 抢购成功!剩余库存:" + stock);
        } else {
            System.out.println(Thread.currentThread().getName() + " 抢购失败!库存不足");
        }
    }
    public static void main(String[] args) {
        StockDemo stock = new StockDemo();
        // 100个用户同时抢购(100个线程)
        for (int i = 0; i < 100; i++) {
            new Thread(stock::buy, "用户" + (i + 1)).start();
        }
    }
}

结果:

用户1 抢购成功!剩余库存:9
用户2 抢购成功!剩余库存:8
...
用户10 抢购成功!剩余库存:0
用户11 抢购成功!剩余库存:-1  // 超卖了!库存为负数
用户12 抢购成功!剩余库存:-2  // 继续超卖

问题原因:多个线程同时进入 if (stock > 0) 判断(比如库存还剩 1 时,5 个线程同时通过判断),之后都执行 stock--,导致库存被多次扣减,出现负数。

加synchronized(线程安全,无超卖)

buy() 方法加 synchronized,或给 “库存检查 + 扣减” 的临界区加锁,保证同一时刻只有一个线程能执行核心逻辑:

public class StockDemo {
    private int stock = 10;
    // 方案1:修饰实例方法(锁对象是当前 StockDemo 实例)
    public synchronized void buy() {
        if (stock > 0) {
            try { Thread.sleep(10); } catch (InterruptedException e) {}
            stock--;
            System.out.println(Thread.currentThread().getName() + " 抢购成功!剩余库存:" + stock);
        } else {
            System.out.println(Thread.currentThread().getName() + " 抢购失败!库存不足");
        }
    }
    // 方案2:修饰代码块(锁对象自定义,粒度更细,推荐)
    /*
    public void buy() {
        synchronized (this) {  // 锁当前实例,也可以用自定义锁对象(如 private final Object lock = new Object();)
            if (stock > 0) {
                try { Thread.sleep(10); } catch (InterruptedException e) {}
                stock--;
                System.out.println(Thread.currentThread().getName() + " 抢购成功!剩余库存:" + stock);
            } else {
                System.out.println(Thread.currentThread().getName() + " 抢购失败!库存不足");
            }
        }
    }
    */
    public static void main(String[] args) {
        StockDemo stock = new StockDemo();
        // 100个用户同时抢购
        for (int i = 0; i < 100; i++) {
            new Thread(stock::buy, "用户" + (i + 1)).start();
        }
    }
}

结果:

用户1 抢购成功!剩余库存:9
用户2 抢购成功!剩余库存:8
...
用户10 抢购成功!剩余库存:0
用户11 抢购失败!库存不足
用户12 抢购失败!库存不足
...
用户100 抢购失败!库存不足

扩展:如果是多实例场景?

如果电商平台部署了多个服务实例(每个实例都是一个 StockDemo 对象),此时 synchronized(对象锁 / 类锁)只能保证 “单个实例内的线程安全”,无法跨实例同步(比如实例 1 的库存 10,实例 2 的库存 10,可能导致总超卖 20 件)。

这种情况需要 分布式锁(如 Redis 分布式锁、ZooKeeper 分布式锁),本质是把 “锁” 放到多个实例都能访问的公共地方(如 Redis),实现跨实例的互斥。

但单实例内的并发安全,synchronized 完全能搞定,且简单高效。

四、锁的实现原理(JVM 层面)

synchronized 早期基于 重量级锁(依赖操作系统内核的互斥量 mutex,切换成本高),JDK 6 后引入 锁优化(偏向锁、轻量级锁、重量级锁),通过 对象头(Mark Word) 存储锁状态:

1. 对象头(Mark Word)结构

Java 对象在内存中分为 3 部分:对象头、实例数据、对齐填充。其中 Mark Word 是实现锁的核心,存储内容随锁状态变化:

锁状态Mark Word 存储内容
无锁对象哈希码、GC 分代年龄、是否偏向锁(0)
偏向锁偏向线程 ID、GC 分代年龄、是否偏向锁(1)
轻量级锁指向线程栈中锁记录(Lock Record)的指针
重量级锁指向 JVM 中监视器锁(Monitor)的指针

2. 锁升级流程(从低开销到高开销)

JVM 按 “竞争程度” 动态升级锁,避免不必要的性能损耗:

  1. 偏向锁:单线程场景下,线程获取锁后,Mark Word 记录线程 ID,后续无需再次竞争(直接复用锁),开销极低。
  2. 轻量级锁:多线程交替执行临界区(无激烈竞争),线程通过 CAS(原子操作) 将 Mark Word 替换为自己的锁记录指针,避免内核态切换。
  3. 重量级锁:多线程同时竞争锁(激烈竞争),CAS 失败的线程阻塞等待(依赖操作系统 mutex),开销最高。

五、关键特性

  1. 可重入性:同一线程可多次获取同一把锁(不会死锁)。例如:
public synchronized void methodA() {
    methodB(); // 同一线程再次获取当前对象锁,允许执行
}
public synchronized void methodB() {}
  1. 原理:锁记录中维护 重入计数器,线程首次获取锁时计数器 = 1,再次获取时计数器 + 1,释放时计数器 - 1,直至为 0 释放锁。
  2. 不可中断性:线程获取锁时(如重量级锁),若锁被占用,线程会进入 BLOCKED 状态,无法被中断(需等待锁释放)。
  3. 非公平锁:默认是 非公平锁(线程释放锁后,等待队列中的线程和新请求锁的线程竞争,新线程可能 “插队”),无法直接改为公平锁(需借助 ReentrantLock)。

六、与 Lock 接口的对比

synchronized 是 JVM 内置锁,java.util.concurrent.locks.Lock 是 API 层面的锁,核心差异如下:

特性synchronizedLock(如 ReentrantLock)
锁实现JVM 内置(C++ 实现)Java 代码实现(API 层面)
锁类型非公平锁(默认)可公平 / 非公平(构造函数指定)
可中断性不可中断(BLOCKED 状态)可中断(lockInterruptibly()
超时获取无(只能无限等待)支持超时获取(tryLock(long, TimeUnit)
条件变量隐式(通过 wait()/notify()显式(Condition 对象,支持多条件)
锁释放自动释放(方法退出 / 代码块结束)必须手动释放(unlock(),需在 finally 中)
性能优化后(偏向 / 轻量级锁)接近 Lock高并发下更灵活(如非阻塞获取)

七、使用注意事项

  • 锁粒度不宜过大:避免对整个方法加锁(尤其是包含非临界区逻辑时),优先使用代码块锁定最小临界区(减少锁竞争)。
  • 锁对象不可变:代码块锁的对象需用 final 修饰(避免锁对象被重新赋值,导致多线程持有不同锁,失去同步效果)。
  • 避免死锁
    • 多个线程获取锁的顺序保持一致(如线程 1 先锁 A 再锁 B,线程 2 也先锁 A 再锁 B);
    • 避免锁嵌套过深;
    • 可使用超时锁(Lock.tryLock())替代 synchronized 防止死锁。
  • 避免锁竞争激烈场景
    • 高并发下,synchronized 重量级锁的性能较差,可考虑用ConcurrentHashMap 等并发容器,或通过分段锁、无锁编程(CAS)优化。

八、总结

synchronized 是 Java 最基础、最常用的线程同步方式,优点是 简单易用、无需手动释放锁、JVM 自动优化,适合解决大多数线程安全问题(如简单的共享变量修改、方法同步)。其核心是通过 “锁对象” 实现互斥访问,结合 JDK 6 后的锁升级机制,在低竞争场景下性能优异,高竞争场景下可考虑用 Lock 接口或并发容器替代,若是多实例场景使用分布式锁。

到此这篇关于Java 同步关键字 synchronized:用法 + 场景 + 避坑指南的文章就介绍到这了,更多相关Java 关键字 synchronized内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 解决java执行cmd命令调用ffmpeg报错Concat error - No such filter ''[0,0]''问题

    解决java执行cmd命令调用ffmpeg报错Concat error - No such filter ''[0,0]

    这篇文章主要介绍了java执行cmd命令,调用ffmpeg报错Concat error - No such filter '[0,0]'解决方法,本文通过截图实例代码说明给大家介绍的非常详细,对大家的工作或学习有一定的参考借鉴价值,需要的朋友可以参考下
    2020-03-03
  • JAVA通过HttpClient发送HTTP请求的方法示例

    JAVA通过HttpClient发送HTTP请求的方法示例

    本篇文章主要介绍了JAVA通过HttpClient发送HTTP请求的方法示例,详细的介绍了HttpClient使用,具有一定的参考价值,有兴趣的可以了解一下
    2017-09-09
  • Java方法递归调用实例解析

    Java方法递归调用实例解析

    这篇文章主要介绍了Java方法递归调用实例解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-02-02
  • Java内存泄漏问题排查与解决

    Java内存泄漏问题排查与解决

    大家好,本篇文章主要讲的是Java内存泄漏问题排查与解决,感兴趣的同学赶快来看一看吧,对你有帮助的话记得收藏一下
    2022-01-01
  • 在JPA中criteriabuilder使用or拼接多个like语句

    在JPA中criteriabuilder使用or拼接多个like语句

    这篇文章主要介绍了在JPA中criteriabuilder使用or拼接多个like语句,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-12-12
  • mybatis主键自增,关联查询,动态sql方式

    mybatis主键自增,关联查询,动态sql方式

    这篇文章主要介绍了mybatis主键自增,关联查询,动态sql方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2025-06-06
  • Java中BM(Boyer-Moore)算法的图解与实现

    Java中BM(Boyer-Moore)算法的图解与实现

    本文主要介绍了两个大的部分,第一部分通过图解的方式讲解BM算法,第二部分则代码实现一个简易的BM算法,感兴趣的小伙伴可以学习一下
    2022-05-05
  • 利用IDEA社区版创建SpringBoot项目的详细图文教程

    利用IDEA社区版创建SpringBoot项目的详细图文教程

    大家应该都知道Idea社区版本,默认是不能创建SpringBoot项目的,下面这篇文章主要给大家介绍了关于利用IDEA社区版创建SpringBoot项目的详细图文教程,文中通过图文介绍的非常详细,需要的朋友可以参考下
    2023-04-04
  • Java匿名类,匿名内部类实例分析

    Java匿名类,匿名内部类实例分析

    这篇文章主要介绍了Java匿名类,匿名内部类,结合实例形式分析了Java匿名类,匿名内部类相关原理、用法及操作注意事项,需要的朋友可以参考下
    2020-04-04
  • Windows下java、javaw、javaws以及jvm.dll等进程的区别

    Windows下java、javaw、javaws以及jvm.dll等进程的区别

    这篇文章主要介绍了Windows下java、javaw、javaws以及jvm.dll等进程的区别,本文分别讲解了它们的作用并给出代码实例,最后做出了区别总结,需要的朋友可以参考下
    2015-03-03

最新评论