synchronized关键字的使用方法和底层原理

一、synchronized的基础使用方法(入门)

synchronized是Java内置的互斥锁,核心作用是保证多线程下同步代码的原子性、可见性和有序性。它有3种核心使用方式,本质都是对“锁对象”的抢占——线程必须获取锁对象的monitor(管程)才能执行同步代码。

1. 修饰实例方法(锁:当前类的实例对象)

  • 锁对象:this(当前调用方法的实例);
  • 特点:不同实例的锁相互独立,同一实例的同步方法互斥。

示例代码:

public class SyncInstanceMethod {
    // 修饰实例方法:锁是当前实例(this)
    public synchronized void print(String name) {
        for (int i = 0; i < 3; i++) {
            System.out.println(name + ":" + i);
            try {
                Thread.sleep(500); // 模拟耗时操作
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        SyncInstanceMethod instance1 = new SyncInstanceMethod();
        SyncInstanceMethod instance2 = new SyncInstanceMethod();

        // 线程1:调用instance1的print(锁instance1)
        new Thread(() -> instance1.print("线程1")).start();
        // 线程2:调用instance1的print(锁instance1,与线程1互斥)
        new Thread(() -> instance1.print("线程2")).start();
        // 线程3:调用instance2的print(锁instance2,与线程1/2不互斥)
        new Thread(() -> instance2.print("线程3")).start();
    }
}

执行结果:

  • 线程1和线程2交替执行(同一实例锁互斥);
  • 线程3与线程1/2并行执行(不同实例锁独立)。

2. 修饰静态方法(锁:当前类的Class对象)

  • 锁对象:类名.class(当前类的Class对象,全局唯一);
  • 特点:所有实例共享这把锁,无论哪个实例调用静态同步方法,都互斥。

示例代码:

public class SyncStaticMethod {
    // 修饰静态方法:锁是SyncStaticMethod.class
    public static synchronized void staticPrint(String name) {
        for (int i = 0; i < 3; i++) {
            System.out.println(name + ":" + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        SyncStaticMethod instance1 = new SyncStaticMethod();
        SyncStaticMethod instance2 = new SyncStaticMethod();

        // 线程1:通过instance1调用静态方法(锁Class对象)
        new Thread(() -> instance1.staticPrint("线程1")).start();
        // 线程2:通过instance2调用静态方法(同一把Class锁,互斥)
        new Thread(() -> instance2.staticPrint("线程2")).start();
    }
}

执行结果:线程1执行完3次后,线程2才开始执行(Class锁全局唯一)。

3. 修饰代码块(锁:自定义对象,最灵活)

  • 锁对象:手动指定(可以是this、类Class对象、任意自定义对象);
  • 特点:精准控制同步范围,减少锁粒度,提升性能。

示例代码(3种锁对象):

public class SyncCodeBlock {
    // 自定义锁对象(推荐:私有、不可变)
    private final Object lockObj = new Object();

    // 锁:this(当前实例)
    public void blockThis(String name) {
        synchronized (this) {
            print(name);
        }
    }

    // 锁:Class对象(全局锁)
    public void blockClass(String name) {
        synchronized (SyncCodeBlock.class) {
            print(name);
        }
    }

    // 锁:自定义对象(局部锁)
    public void blockCustom(String name) {
        synchronized (lockObj) {
            print(name);
        }
    }

    private void print(String name) {
        for (int i = 0; i < 3; i++) {
            System.out.println(name + ":" + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        SyncCodeBlock instance = new SyncCodeBlock();
        // 线程1:锁this
        new Thread(() -> instance.blockThis("线程1")).start();
        // 线程2:锁Class对象(与线程1不互斥,锁对象不同)
        new Thread(() -> instance.blockClass("线程2")).start();
        // 线程3:锁自定义对象(与线程1/2不互斥)
        new Thread(() -> instance.blockCustom("线程3")).start();
    }
}

执行结果:三个线程并行执行(锁对象不同,无互斥)。

二、synchronized的核心特性(理解锁行为)

掌握用法后,需理解synchronized的核心特性,这是区分它与其他锁(如ReentrantLock)的基础。

1. 可重入性(避免同一线程死锁)

定义:同一线程多次获取同一把锁时,不会死锁,锁会记录“重入次数”,释放次数需与获取次数一致。

  • 底层实现:JVM通过monitor的“拥有者线程”和“重入计数器”实现(计数器初始0,获取锁+1,释放锁-1,计数器为0时真正释放)。

示例代码(验证可重入):

public class SyncReentrant {
    public synchronized void method1() {
        System.out.println("method1:获取锁,计数器=1");
        method2(); // 同一线程再次获取同一把锁(this)
    }

    public synchronized void method2() {
        System.out.println("method2:重入锁,计数器=2");
        method3();
    }

    private synchronized void method3() {
        System.out.println("method3:重入锁,计数器=3");
    }

    public static void main(String[] args) {
        new SyncReentrant().method1(); // 无死锁,正常执行
    }
}

执行结果:三个方法依次执行,无死锁,验证了可重入性。

2. 不可中断性(等待锁时无法被中断)

定义:线程等待获取synchronized锁时,会进入“阻塞状态”,即使调用Thread.interrupt()标记中断状态,也无法唤醒线程,只能继续等待锁释放。

示例代码(验证不可中断):

public class SyncUninterruptible {
    private static final Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        // 线程1:先获取锁,持有10秒
        Thread t1 = new Thread(() -> {
            synchronized (lock) {
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.start();
        Thread.sleep(100); // 确保t1先获取锁

        // 线程2:等待锁,尝试中断
        Thread t2 = new Thread(() -> {
            System.out.println("t2:开始等待锁...");
            synchronized (lock) { // 进入阻塞,等待锁
                System.out.println("t2:获取到锁(如能中断该行代码则不会执行)");
            }
        });
        t2.start();
        Thread.sleep(1000);
        t2.interrupt(); // 中断t2
        System.out.println("t2:中断状态=" + t2.isInterrupted()); // true,但仍等待锁
    }
}

执行结果:t2中断状态为true,但仍会阻塞到t1释放锁,验证了不可中断性。

3. 非公平性(无法保证先来先得)

定义:synchronized是固定的非公平锁——新线程尝试获取锁时,会先“插队”(直接抢占monitor),而非排队;即使有线程在等待队列中,新线程也可能优先获取锁,无法保证等待线程的获取顺序。

  • 底层缘由:JVM唤醒等待线程时,是从_EntryList(等待队列)中随机选择线程,而非按FIFO顺序。

三、synchronized的底层实现原理(JVM层面)

这是深度理解的核心,synchronized的底层依赖两个核心:对象头(Mark Word)Monitor(管程)

1. 前提:Java对象的内存布局

每个Java对象在堆内存中包含3部分:

对象头(Object Header):Mark Word + 类型指针(Klass Pointer)
实例数据(Instance Data):对象的成员变量
对齐填充(Padding):保证对象大小是8字节的整数倍

其中,Mark Word 是实现锁的核心,它存储了对象的锁状态、拥有者线程ID、重入计数器等信息,结构如下(32位JVM):

锁状态

存储内容(Mark Word)

无锁状态

对象哈希码 + 分代年龄 + 无锁标记(01)

偏向锁状态

偏向线程ID + 偏向时间戳 + 分代年龄 + 偏向标记(01)

轻量级锁状态

指向栈中锁记录的指针 + 轻量级锁标记(00)

重量级锁状态

指向Monitor的指针 + 重量级锁标记(10)

标记为垃圾

空 + 垃圾标记(11)

2. Monitor(管程)机制(核心)

synchronized的互斥性依赖Monitor(由JVM的C++层实现,对应ObjectMonitor类),每个对象都关联一个Monitor,只有获取到Monitor的线程才能执行同步代码。

ObjectMonitor的核心字段(简化):

// C++层面的ObjectMonitor结构(JVM源码)
class ObjectMonitor {
    _owner;       // 持有锁的线程(NULL表明无线程持有)
    _EntryList;   // 等待获取锁的线程队列(阻塞状态)
    _WaitSet;     // 调用wait()后等待的线程队列
    _recursions;  // 重入计数器(对应可重入性)
    _count;       // 锁的竞争次数
};

Monitor的核心工作流程:

synchronized关键字的使用方法和底层原理

3. 字节码层面的体现

synchronized编译后会生成monitorenter和monitorexit指令,分别对应“获取锁”和“释放锁”。

示例:反编译同步代码块的字节码

// 源码
public class SyncBytecode {
    public void test() {
        synchronized (this) {
            System.out.println("同步代码块");
        }
    }
}

编译后通过javap -v SyncBytecode.class反编译,核心字节码:

public void test();
  Code:
     0: aload_0             // 加载this到操作数栈
     1: dup                 // 复制this引用
     2: astore_1            // 存储到局部变量表
     3: monitorenter        // 获取this对应的Monitor(加锁)
     4: getstatic     #2    // 获取System.out
     7: ldc           #3    // 加载字符串"同步代码块"
     9: invokevirtual #4    // 调用println方法
    12: aload_1            // 加载局部变量表中的this
    13: monitorexit        // 释放Monitor(解锁)
    14: goto          22    // 正常结束,跳转到22行
    17: astore_2            // 异常处理:存储异常
    18: aload_1            // 加载this
    19: monitorexit        // 异常时也释放锁(避免死锁)
    20: aload_2            // 抛出异常
    21: athrow              // 异常抛出
    22: return              // 方法返回

关键结论:

  • monitorenter:线程尝试获取Monitor,失败则阻塞;
  • monitorexit:释放Monitor,且编译器会在异常路径中也插入monitorexit(保证锁自动释放)。

四、JDK 1.6后的锁优化(性能提升核心)

JDK 1.6前,synchronized是“重量级锁”(直接调用操作系统互斥量,性能差);JDK 1.6引入了偏向锁、轻量级锁等优化,让synchronized性能大幅提升。

1. 锁状态升级流程(不可逆)

无锁 → 偏向锁 → 轻量级锁 → 重量级锁
  • 升级触发条件:根据锁的竞争程度,从低开销到高开销逐步升级。

2. 各锁状态的核心逻辑

锁状态

适用场景

实现原理

性能特点

偏向锁

无竞争/单线程重复加锁

标记锁的偏向线程ID,后续该线程加锁无需CAS,直接获取

开销最小(仅标记ID)

轻量级锁

少量线程竞争(短时间)

线程通过CAS自旋获取锁,避免内核态切换

开销中等(CAS自旋)

重量级锁

大量线程竞争(长时间)

线程进入内核态阻塞,依赖操作系统互斥量

开销最大(上下文切换)

3. 关键优化:自旋锁与适应性自旋

  • 自旋锁:轻量级锁竞争时,线程不立即阻塞,而是通过CAS循环(自旋)尝试获取锁,减少内核态切换;
  • 适应性自旋:JVM根据前一次自旋的成功率,动态调整自旋次数(如上次成功则增加次数,失败则减少),提升自旋效率。

五、实战注意事项(避坑)

1. 锁对象的选择(核心避坑点)

  • ❌ 错误:使用String常量、Integer等包装类常量(如synchronized (“lock”))——常量池复用导致锁失效;
  • ❌ 错误:使用可变对象(如synchronized (new ArrayList<>()))——每次创建新对象,锁失去互斥性;
  • ✅ 正确:使用private final Object lock = new Object()(私有、不可变、专属锁对象)。

2. 锁粒度控制

  • 锁粒度过大:同步整个方法(如public synchronized void bigMethod()),导致并发度低;
  • 锁粒度过小:过度拆分同步代码块,可能导致线程安全问题;
  • ✅ 原则:只同步“需要原子性的核心代码”,缩小同步范围。

3. 与volatile的区别(补充)

特性

synchronized

volatile

原子性

保证

不保证

可见性

保证

保证

有序性

保证

保证(禁止重排序)

适用场景

同步代码块/方法

修饰变量(状态标记)

总结(核心要点回顾)

  1. 使用层面:synchronized有3种用法(实例方法、静态方法、代码块),核心是“锁对象”——不同锁对象对应不同的互斥范围;
  2. 特性层面:具备可重入、不可中断、非公平性,异常时自动释放锁;
  3. 底层层面:依赖对象头(Mark Word)和Monitor机制,JDK 1.6后通过偏向锁/轻量级锁优化性能;
  4. 实战层面:锁对象需选私有不可变对象,控制锁粒度,避免常量锁/可变锁陷阱。

理解这些内容后,你不仅能正确使用synchronized,还能从底层解释其行为,在高并发场景下做出合理的锁设计。

© 版权声明

相关文章

暂无评论

none
暂无评论...