Synchronized:是什么、作用原理及在Java中的使用

作者: IT Sectr 发布日期: 2026-03-19 阅读时间: 8 分钟

Synchronized是Java语言中内置的同步机制,提供对代码关键部分的独占访问。根据Oracle, 2024,synchronized修饰符保证只有一个线程可以在特定时刻执行标记的方法或块。该机制基于监视器——操作系统的基本概念,确保多线程应用程序在所有复杂级别上正确运行。

要点

  • Synchronized — 用于线程安全数据访问的Java关键字。
  • 对象监视器 — 同步所基于的内部机制。
  • Synchronized方法 在实例或类级别锁定整个方法。
  • Synchronized块 允许仅同步部分代码。
  • 死锁 — 嵌套同步中的主要问题之一。

什么是synchronized?

Synchronized是Java中的一个关键字,保证一次只有一个线程执行受保护的代码部分,防止并行访问时的数据损坏。它出现在Java的第一个版本中,并且仍然是各种水平的开发人员确保线程安全的最简单方法。

定义及其在Java中的作用

Synchronized修饰符解决两个任务:互斥(mutual exclusion)和变更可见性(visibility)。当线程离开synchronized块时,所有更改保证对进入在同一对象上同步的块的其他线程可见。

Synchronized可以应用于整个方法或任意代码块,并指定监视器对象。在这两种情况下,JVM在字节码级别插入monitorenter和monitorexit指令。

出现的背景

在没有同步的多线程应用程序中,会出现竞态条件(race condition)——当两个线程同时修改相同数据时,导致不可预测的结果。Synchronized成为Java解决这个问题的第一个和主要工具,为每个开发人员提供简单声明式语法。

Synchronized是如何工作的?

Synchronized机制基于监视器的概念——嵌入到每个Java对象中的高级同步原语。监视器在第一次对对象使用synchronized块时与该对象关联。

对象监视器

Java中的每个对象都有一个关联的监视器。当线程进入synchronized块时,它获取对象的监视器。如果监视器已经被另一个线程占用,该线程将被阻塞直到释放。在字节码中,这对应于monitorenter和monitorexit指令对。

锁定状态(偏向锁)

JVM通过多个级别优化synchronized:偏向锁用于单线程访问,轻量级锁用于低竞争,重量级锁用于涉及操作系统的高强度竞争。这些级别在不改变代码的情况下提高性能。

java
class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

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

Happens-before规则

Synchronized建立happens-before关系:线程在离开synchronized块之前的所有操作,对之后进入在同一对象上同步的块的另一个线程可见。这不仅保证互斥,还保证所有线程的数据一致性。

Synchronized方法与块

Java提供两种应用synchronized的方式:在方法级别和在块级别。选择会影响性能和同步粒度。

Synchronized方法

使用synchronized修饰符标记方法,会自动在当前实例上(对于普通方法)或在Class对象上(对于静态方法)同步它。这是确保互斥的最简单方法,但如果关键部分只占方法的很小一部分而其余代码不需要同步,则通常是多余的。

Synchronized块

Synchronized块提供精确控制:指定监视器对象并仅同步所需的代码部分,将方法的其余部分保留在锁定之外。这最小化了监视器的持有时间,并在多线程环境中提高了应用程序的整体性能,因为其他线程可以并行执行不相关的代码,而无需等待监视器释放。

java
class DataProcessor {
    private final Object lock = new Object();

    public void process() {
        // 临界区外的代码 - 无同步
        prepareData()

        synchronized (lock) {
            // 只有这个块受到保护
            updateSharedState()
        }

        // 无锁继续
        cleanup()
    }
}
标准Synchronized方法Synchronized块
监视器this(实例)或Class任何对象
粒度整个方法仅需要的代码
可读性中等
性能大方法较低小关键部分较高

Android中的Synchronized

在Android开发中,synchronized广泛用于保护SharedPreferences、数据库访问和UI组件。然而,由于界面冻结的风险,强烈不推荐在主线程上使用它。

与SharedPreferences的使用

Android中的SharedPreferences提供基本的线程安全性,但通过Editor由多个线程编辑时,可能需要外部同步。带有单独锁定对象的synchronized块保证更改的一致性。

kotlin
class PreferencesManager(private val prefs: SharedPreferences) {
    private val lock = Any()

    fun writeToken(token: String) {
        synchronized (lock) {
            prefs.edit()
                .putString("auth_token", token)
                .apply()
        }
    }
}

Android应用程序中的限制

Android中synchronized的主要限制是线程阻塞。与带有Mutex的协程不同,synchronized会阻塞整个系统线程。在主线程上,这会导致ANR。在现代Android开发中,建议用协程(suspend Mutex)或原子类型(AtomicInteger)替换synchronized。

Synchronized的替代方案

现代Java和Kotlin提供了几种synchronized的替代方案,每种方案都以更少的限制或更好的性能解决相同的任务。

来自java.util.concurrent的Lock

Lock接口及其实现ReentrantLock和ReadWriteLock提供超时、可中断等待和多个Condition队列。它比synchronized更灵活,但需要在finally中显式释放,这增加了忘记解锁时的错误风险。

原子类

AtomicInteger、AtomicLong、AtomicReference等类使用基于CAS(Compare-And-Swap)的无锁算法。在中等竞争场景中,它们明显比synchronized快,因为它们不会阻塞线程,而是执行乐观重试,并且不需要操作系统内核进行上下文切换。

ThreadLocal与线程安全

ThreadLocal提供了一种替代方法:每个ThreadLocal变量在一个线程范围内被隔离,无需同步即可读写。这完全消除了对不应该在线程间共享的数据进行synchronized的需要。ThreadLocal在框架(Spring、Hibernate)中积极用于存储事务上下文和会话。

协程与compose方法

在Android的Kotlin项目中,synchronized的替代方案是来自kotlinx.coroutines的Mutex。它不会阻塞操作系统线程,而是挂起协程直到锁定释放——这使得能够有效利用池线程,并在等待资源释放时避免ANR。

kotlin
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

val mutex = Mutex()
var counter = 0

suspend fun safeIncrement() {
    mutex.withLock {
        counter++
    }
}

Synchronized的性能

Synchronized的性能在最近的Java版本中发生了显著变化。以前被认为是一种"沉重"的机制,但现代JVM通过JIT编译器的先进优化消除了大部分开销。让我们详细看看虚拟机如何在运行时加速同步代码。

偏向锁与锁粗化

JVM的JIT编译器应用了几种优化:偏向锁在锁总是被一个线程获取时消除同步;锁粗化将相邻的synchronized块合并为一个;锁消除在对象只对一个线程可访问时移除同步。这些优化使synchronized在低竞争下几乎免费。

竞争测量与优化选择

JVM确定每个对象的竞争级别:在无竞争时启用偏向锁,在第二个线程出现时锁转为带自旋等待的轻量级模式,只有在长时间等待时才转为带系统互斥量的重量级。这种升级是自动发生的,开发人员不需要手动选择策略。

与Lock和原子类的比较

在现代基准测试中(Java 17+),synchronized在低和中等竞争下表现出与ReentrantLock相当的性能。在高竞争下,Lock由于更高效的支持超时和中断的等待队列而可能具有优势。对于竞争恒定的高负载系统,公平模式下的ReentrantLock提供更可预测的行为。

原子类(AtomicInteger、AtomicReference)由于基于CAS的无锁实现,对于简单计数器和标志仍然是最快的。它们根本不会阻塞线程——发生冲突时,操作只是在循环中重试。在4-8个线程的计数器增量操作中,与synchronized相比,这提供了3-5倍的性能提升。

常见问题

Synchronized和volatile有什么区别?

Synchronized既提供互斥也提供可见性。Volatile只保证变更的可见性——对volatile变量的写入对所有线程可见,但不能防止同时修改,即不保护竞态条件。

Synchronized会导致死锁吗?

是的,死锁在具有不同监视器顺序的嵌套同步中是可能的。例如,一个线程调用synchronized(a) { synchronized(b) },另一个调用synchronized(b) { synchronized(a) }。避免嵌套的synchronized块,或确定统一的监视器顺序。

Java中的监视器是什么?

监视器是一种与每个Java对象关联的同步机制。它保证只有一个线程在该对象上执行synchronized代码。监视器包括锁、等待队列和等待通过wait/notify通知的线程池。

Lock比synchronized快吗?

在现代Java版本中(17+),synchronized凭借JIT优化(偏向锁、锁粗化)在性能上不逊于Lock。选择Lock不是因为速度,而是因为额外功能:超时、可中断等待和多个Condition。

Synchronized如何与静态方法一起使用?

静态synchronized方法使用该类的Class对象的监视器,而不是实例。这意味着同步覆盖该类的所有实例。非静态和静态synchronized方法使用不同的监视器,不会互相阻塞。

总结

  • Synchronized — 基于监视器的内置Java同步机制。
  • 对象监视器 — 确保互斥的JVM内部结构。
  • wait/notify方法 仅在synchronized块或方法内使用。
  • Synchronized块 由于更细粒度的同步而优于方法。
  • Happens-before 在同一对象上同步时保证线程间变更可见性。
  • 死锁 — 不同监视器顺序嵌套同步中的主要风险。
  • 替代方案 — Lock、原子类和带suspend Mutex的协程。

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读