导读:本期聚焦于天马创作的《在Java里如何实现线程同步控制_Java synchronized关键点解析》,敬请观看详情。多个线程同时读写共享资源时,数据错乱几乎是必现的问题。Java提供的synchronized关键字是最基础也最常用的同步手段,它依靠对象头的监视器锁保证同一时刻只有一个线程能进入临界区。本文围绕synchronized展开,先讲清同步方法和同步代码块在字节码层面的差异,再分析monitor与对象头Mark Word的关系,说明偏向锁、轻量级锁、重量级锁的升级过程,最后结合生产者消费者、双重检查锁定单例等典型场景给出可运行代码,并对比synchronized与ReentrantLock的适用边界,帮助读者写出既正确又高效的并发程序。

线程同步是Java并发编程里绕不开的话题。当多个线程同时修改同一个共享变量时,如果没有同步措施,结果往往不可预期。synchronized是Java语言层面提供的同步机制,使用简单、不易出错,但它背后的锁原理、锁升级过程以及使用上的细节,很多开发者只停留在表面。本文从用法、原理、典型场景和常见陷阱几个角度,把synchronized的关键点讲透。

在Java里如何实现线程同步控制_Java synchronized关键点解析

一、synchronized的两种基本用法

synchronized可以修饰方法,也可以修饰代码块。修饰实例方法时,锁的是当前对象实例this;修饰静态方法时,锁的是类的Class对象;修饰代码块时,锁的是括号里指定的对象。这三种方式的锁对象各不相同,理解这一点是正确使用synchronized的第一步。

先看一个典型的线程不安全例子:

public class Counter {
    private int count = 0;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

count++看起来是一行代码,实际上包含读取、加一、写回三个步骤。多个线程交错执行时,部分更新就会丢失。加锁后的写法如下:

public class Counter {
    private int count = 0;

    // 方式一:同步方法,锁是当前实例
    public synchronized void increment() {
        count++;
    }

    // 方式二:同步代码块,锁是显式指定的对象
    public void increment2() {
        synchronized (this) {
            count++;
        }
    }

    public int getCount() {
        return count;
    }
}

两种方式效果相同,但同步代码块更细粒度,只锁必要的部分。如果方法里只有几行涉及共享变量,其余都是耗时操作(比如IO),把整个方法标记为synchronized会白白降低并发度。此外要注意,静态同步方法锁的是Class对象,一个线程进入静态同步方法,另一个线程仍然可以进入同一个类的实例同步方法,因为它们锁的不是同一个对象。

二、锁的底层原理:monitor与Mark Word

synchronized的实现依赖对象头中的Mark Word和monitor监视器。每个Java对象都关联一个monitor,在HotSpot虚拟机中由ObjectMonitor实现。线程进入同步块时,执行monitorenter指令尝试获取monitor所有权;退出时执行monitorexit释放。同步方法则不同,它没有显式的字节码指令,而是通过方法的ACC_SYNCHRONIZED访问标志,由调用方法的过程隐式处理加锁解锁。

可以通过javap -v命令查看字节码验证这一点。同步代码块编译后能看到monitorenter和monitorexit成对出现,同步方法则只是在方法flags里多了一个synchronized标志。monitor内部维护着几个关键字段:_owner指向持有线程,_EntryList存放阻塞等待的线程,_WaitSet存放调用wait()后进入等待状态的线程。这也是wait和notify必须放在synchronized块内调用的原因,它们本质上是操作monitor的等待队列。

对象头中的Mark Word存储了锁状态信息。由于对象头空间有限,Mark Word采用复用设计,不同锁状态下存储的内容不同:无锁态存对象哈希码和分代年龄,偏向锁态存偏向线程ID,轻量级锁态存指向栈中锁记录的指针,重量级锁态存指向monitor的指针。锁的状态不是一成不变的,而是随着竞争情况逐步升级。

三、偏向锁、轻量级锁与重量级锁的升级过程

JDK 1.6之后,HotSpot对synchronized做了大量优化,引入了锁升级机制。偏向锁是最乐观的假设:如果一把锁始终由同一个线程获取,就没有必要反复CAS。线程第一次获取锁时,通过CAS把自己的线程ID写入Mark Word,之后该线程再进入同步块只需比对线程ID即可,几乎零成本。

一旦有第二个线程来竞争,偏向锁升级为轻量级锁。竞争线程会在自己的栈帧里创建锁记录,然后CAS尝试把Mark Word指向锁记录。获取成功的线程持有锁,失败的线程自旋等待。轻量级锁适合线程交替执行、锁持有时间很短的场景,自旋几次就能拿到锁,避免了线程挂起的开销。

如果自旋超过阈值或者有第三个线程同时竞争,锁膨胀为重量级锁。重量级锁依赖操作系统的互斥量,竞争失败的线程会被挂起并放入monitor的_EntryList中,涉及用户态到内核态的切换,开销较大。除了锁升级,JVM还有锁消除和锁粗化两项优化:锁消除指JIT通过逃逸分析发现锁对象不可能被共享时直接去掉锁;锁粗化指连续对同一对象加锁解锁时,把多个同步块合并成一个。理解这些机制后就知道,不必因为早期synchronized性能差的说法而回避它,现代JVM中它的性能已经很好,绝大多数场景下与显式锁差距很小。

四、典型应用场景与常见陷阱

最经典的应用是生产者消费者模型,利用wait和notify实现线程间协作:

public class TaskQueue {
    private final Queue<Integer> queue = new LinkedList<>();
    private final int MAX_SIZE = 10;

    public synchronized void put(int value) throws InterruptedException {
        while (queue.size() == MAX_SIZE) {
            wait(); // 队列满,等待消费者取走
        }
        queue.offer(value);
        notifyAll(); // 唤醒消费者
    }

    public synchronized int take() throws InterruptedException {
        while (queue.isEmpty()) {
            wait(); // 队列空,等待生产者放入
        }
        int value = queue.poll();
        notifyAll(); // 唤醒生产者
        return value;
    }
}

注意这里的等待条件判断用的是while而不是if。被唤醒的线程重新获得锁后,条件可能又被其他线程改变了,必须再次检查,否则会出现虚假唤醒导致的逻辑错误。notifyAll虽然比notify效率低一点,但更安全,notify只随机唤醒一个线程,可能唤醒了错误角色的线程导致所有线程都在等待。

另一个常见场景是双重检查锁定的单例模式:

public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

这里volatile不是多余的。new Singleton()分为分配内存、初始化、赋值引用三步,指令重排序可能让instance先指向未初始化的内存,另一个线程在第一次检查时拿到非null但未初始化完成的对象。volatile禁止了这种重排序。此外还有几个常见陷阱需要提醒:一是不要用String或Integer等不可变类型实例作为锁对象,字符串常量和缓存的小整数可能被其他代码引用,造成锁被意外共享;二是不要在持有锁时调用外部回调或耗时方法,容易造成长时间阻塞甚至死锁;三是加锁顺序要保持一致,多个线程以不同顺序获取多把锁是死锁的典型成因,排查时可以用jstack命令打印线程dump查看死锁信息。

五、synchronized与ReentrantLock如何选择

有了synchronized为什么还需要ReentrantLock?两者都是可重入锁,但ReentrantLock提供了更灵活的能力:支持公平锁、可中断获取锁、超时获取锁,以及一个锁上绑定多个等待条件的Condition。当需要这些高级特性时,比如要实现一个带有等待时间的资源获取,ReentrantLock更合适。

synchronized的优势在于简洁和自动释放。线程执行完同步块或抛出异常时,锁自动释放,不存在忘记unlock的问题;而ReentrantLock必须在finally块中手动调用unlock,一旦遗漏就是事故。此外经过锁升级优化后,synchronized在低竞争场景下性能出色。经验法则是:默认用synchronized,只有当明确需要公平性、可中断、超时或多个等待条件时,再换用ReentrantLock。选型不必过度纠结,先把同步逻辑写对,再谈优化。

Java线程同步synchronized锁机制修改时间:2026-09-05 03:30:32

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260905/50647.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。