时间:2026-03-01 16:12
人气:
作者:admin
public class InstanceSyncDemo { private int count = 0; // 修饰实例方法,锁是当前对象this public synchronized void increment() { count++; System.out.println(Thread.currentThread().getName() + " - count: " + count); } public static void main(String[] args) { InstanceSyncDemo demo = new InstanceSyncDemo(); // 创建5个线程并发调用increment方法 for (int i = 0; i < 5; i++) { new Thread(() -> { demo.increment(); }, "Thread-" + i).start(); } } }
在上述代码中,
increment方法被synchronized修饰,当多个线程尝试调用demo.increment()时,由于锁的存在,它们会依次执行,不会出现竞态条件导致count变量的更新错误。
修饰静态方法:当 synchronized 修饰静态方法时,它锁定的是当前类的 Class 对象。因为静态方法属于类,而不是类的实例,所以无论创建多少个类的实例对象,在同一时刻,只能有一个线程能够进入并执行该同步静态方法。这种方式适用于对类的静态变量或全局资源进行同步访问的场景,保证所有实例对这些资源的操作是线程安全的。例如:
public class StaticSyncDemo { private static int total = 0; // 修饰静态方法,锁是当前类的Class对象 public static synchronized void addTotal() { total++; System.out.println(Thread.currentThread().getName() + " - total: " + total); } public static void main(String[] args) { // 创建5个线程并发调用addTotal方法 for (int i = 0; i < 5; i++) { new Thread(() -> { StaticSyncDemo.addTotal(); }, "Thread-" + i).start(); } } }
在这段代码中,
addTotal方法是静态同步方法,多个线程调用StaticSyncDemo.addTotal()时,会竞争类的 Class 对象锁,从而保证了total静态变量的线程安全更新。
修饰代码块:synchronized 修饰代码块时,可以更加灵活地控制锁的范围和锁对象。它可以指定任意对象作为锁,当线程进入同步代码块时,会获取指定对象的锁,执行完代码块后释放锁。这种方式适用于只需要对部分代码进行同步,或者需要针对不同的资源使用不同锁的场景,能够有效减少锁的粒度,提高并发性能。例如:
public class BlockSyncDemo { private Object lock = new Object(); private List<String> list = new ArrayList<>(); public void addElement(String element) { // 非同步操作,可以并行执行 System.out.println(Thread.currentThread().getName() + " is preparing to add element..."); // 同步代码块,锁是lock对象 synchronized (lock) { list.add(element); System.out.println(Thread.currentThread().getName() + " added element: " + element); } // 非同步操作,可以并行执行 System.out.println(Thread.currentThread().getName() + " finished adding element."); } public static void main(String[] args) { BlockSyncDemo demo = new BlockSyncDemo(); // 创建5个线程并发调用addElement方法 for (int i = 0; i < 5; i++) { new Thread(() -> { demo.addElement("Element-" + i); }, "Thread-" + i).start(); } } }
在上述代码中,
addElement方法中的同步代码块使用lock对象作为锁,只有获取到lock锁的线程才能执行代码块内的list.add(element)操作,保证了list集合的线程安全操作,而方法中的其他非同步代码可以并行执行,提高了程序的并发性能。
count++这样的复合操作,在多线程环境下如果不进行同步,可能会出现数据不一致的情况,但使用 synchronized 修饰包含count++的方法或代码块后,就可以保证这个操作是原子的,要么完整执行,要么不执行 。
可见性:当一个线程释放 synchronized 锁时,会将其工作内存中的变量值刷新到主内存中;而当另一个线程获取到该锁时,会从主内存中读取最新的变量值。这就保证了不同线程之间对共享变量的可见性,使得一个线程对共享变量的修改能够及时被其他线程看到。例如,在一个多线程程序中,线程 A 修改了共享变量data,并释放了锁,那么线程 B 在获取锁后,就能够读取到线程 A 修改后的data值。
有序性:synchronized 通过内存屏障和 happens-before 规则保证了一定程度的有序性。它确保了在释放锁之前的所有操作,对于随后获取同一把锁的线程来说都是可见的,并且这些操作不会被重排序到锁的范围之外。例如,线程 A 在持有锁的情况下执行了操作 1 和操作 2,然后释放锁,线程 B 获取锁后,一定能按照线程 A 执行的顺序看到操作 1 和操作 2 的结果 。
通过保证原子性、可见性和有序性,synchronized 有效地解决了多线程环境下的数据竞争问题,确保了程序在并发场景下的正确性和稳定性。无论是在简单的多线程数据访问,还是复杂的并发业务逻辑处理中,synchronized 都发挥着不可或缺的作用,为开发者提供了一种简单而强大的线程同步机制。
User,包含一个int类型的id和一个String类型的name:
public class User { private int id; private String name; public User(int id, String name) { this.id = id; this.name = name; } }
在 64 位 JVM 且开启指针压缩的情况下,
User对象的内存布局如下:对象头(Mark Word 8 字节 + 类型指针 4 字节 = 12 字节),实例数据(int类型的id 4 字节 + String引用类型 4 字节 = 8 字节),总大小为 20 字节,不是 8 的倍数,因此需要 4 字节的对齐填充,最终User对象占用 24 字节的内存空间 。
对象头中的 Mark Word 对于 synchronized 锁机制起着核心作用,它存储的锁状态标志位等信息,直接决定了对象当前的锁状态,进而影响着线程对对象的访问方式和同步控制 。
hashCode()方法时生成并存储在此;分代年龄则记录了对象经历垃圾回收的次数,用于判断对象是否应该晋升到老年代。Object obj = new Object();为例,在初始状态下,它处于无锁状态,Mark Word 存储着对象的哈希码和分代年龄等信息。当一个线程首次访问synchronized(obj)代码块时,如果偏向锁开启(JDK 1.6 及以后默认开启),对象会进入偏向锁状态,Mark Word 中记录下该线程的 ID。若此时有另一个线程也尝试访问该同步块,就会发生锁竞争,偏向锁失效,锁升级为轻量级锁,Mark Word 的内容也相应变为指向新线程栈中锁记录的指针。如果竞争进一步加剧,轻量级锁自旋一定次数后仍无法获取锁,就会升级为重量级锁,Mark Word 指向 Monitor 对象 。
Mark Word 的这种动态变化机制,使得 Java 虚拟机能够根据不同的竞争情况,灵活地选择合适的锁策略,从而在保证线程安全的前提下,尽可能地提高性能。
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.17</version>
</dependency>
然后,编写如下代码:
import org.openjdk.jol.info.ClassLayout; public class SynchronizedObjectHeaderDemo { public static void main(String[] args) throws InterruptedException { Object lock = new Object(); // 查看初始状态下对象头信息 System.out.println("初始状态下对象头信息:"); System.out.println(ClassLayout.parseInstance(lock).toPrintable()); synchronized (lock) { // 查看加锁后对象头信息 System.out.println("\n加锁后对象头信息:"); System.out.println(ClassLayout.parseInstance(lock).toPrintable()); } // 查看解锁后对象头信息 System.out.println("\n解锁后对象头信息:"); System.out.println(ClassLayout.parseInstance(lock).toPrintable()); } }
在上述代码中,我们创建了一个 Object 对象
lock,并在三个阶段分别打印其对象头信息:初始状态、加锁后以及解锁后。
运行代码,输出结果类似如下:
初始状态下对象头信息: java.lang.Object object internals: OFF SZ TYPE DESCRIPTION VALUE 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 加锁后对象头信息: java.lang.Object object internals: OFF SZ TYPE DESCRIPTION VALUE 0 4 (object header) 00 f0 0b 00 (00000000 11110000 00001011 00000000) (112640) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 解锁后对象头信息: java.lang.Object object internals: OFF SZ TYPE DESCRIPTION VALUE 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
从输出结果可以看出,初始状态下,对象头的 Mark Word 处于无锁状态。当进入 synchronized 代码块加锁后,Mark Word 的内容发生了变化,表明锁状态已改变,此时 Mark Word 指向了与 Monitor 相关的信息(具体值根据实际情况而定) 。当退出 synchronized 代码块解锁后,Mark Word 又恢复到了无锁状态 。 结合 Monitor 原理分析,当线程进入 synchronized (lock) 代码块时,会尝试获取
lock对象关联的 Monitor 的锁。如果获取成功,对象头的 Mark Word 会记录相关的锁信息,指向 Monitor 对象。在同步块执行期间,其他线程若尝试获取锁,会进入 Monitor 的_EntryList 等待。当线程执行完同步块释放锁时,Mark Word 的锁信息被清除,恢复到无锁状态,同时 Monitor 会根据_WaitSet 和_EntryList 的情况,决定是否唤醒其他等待的线程 。通过这个示例,我们可以清晰地看到对象头与 Monitor 在 synchronized 机制中的紧密协作关系。
public class LockUpgradeDemo { private static final Object lock = new Object(); public static void main(String[] args) throws InterruptedException { // 开启偏向锁,JDK 17之前有效 // -XX:+UseBiasedLocking // 打印对象头信息,用于观察锁状态 // -XX:+PrintObjectLayout // 第一个线程获取锁,触发偏向锁 Thread thread1 = new Thread(() -> { synchronized (lock) { System.out.println("Thread1 获取锁,此时应为偏向锁"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); thread1.start(); thread1.join(); // 第二个线程获取锁,偏向锁失效,升级为轻量级锁 Thread thread2 = new Thread(() -> { synchronized (lock) { System.out.println("Thread2 获取锁,此时应为轻量级锁"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); thread2.start(); thread2.join(); // 多个线程同时竞争锁,轻量级锁升级为重量级锁 for (int i = 0; i < 5; i++) { new Thread(() -> { synchronized (lock) { System.out.println(Thread.currentThread().getName() + " 获取锁,此时应为重量级锁"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }, "Thread-" + i).start(); } } }
在上述代码中,我们创建了一个 Object 对象
lock作为锁对象 。首先,thread1线程获取锁,此时对象应该处于偏向锁状态 。然后,thread2线程尝试获取锁,这会导致偏向锁失效,锁升级为轻量级锁 。最后,通过启动 5 个线程同时竞争锁,模拟高并发场景,使轻量级锁升级为重量级锁 。
为了观察锁状态的变化,我们可以在运行代码时添加相应的 JVM 参数 。在 JDK 17 之前,可以使用-XX:+UseBiasedLocking开启偏向锁,使用-XX:+PrintObjectLayout打印对象头信息,从而查看对象在不同阶段的锁状态 。在 JDK 17 及之后,虽然偏向锁已被移除,但仍然可以通过-XX:+PrintObjectLayout来观察轻量级锁和重量级锁的状态变化 。通过这种方式,我们可以更加深入地理解 synchronized 的锁升级机制 。
public class AtomicityDemo { private static int sharedVariable = 0; public static void increment() { synchronized (AtomicityDemo.class) { sharedVariable++; } } public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(() -> { for (int i = 0; i < 1000; i++) { increment(); } }); Thread thread2 = new Thread(() -> { for (int i = 0; i < 1000; i++) { increment(); } }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println("最终的共享变量值: " + sharedVariable); } }
在上述代码中,
increment方法通过synchronized关键字修饰,当thread1线程执行到synchronized (AtomicityDemo.class)时,会执行 monitorenter 指令,尝试获取AtomicityDemo.class对象的锁 。如果获取成功,其他线程(如thread2)就无法同时进入该同步块 。在同步块内执行sharedVariable++操作,这个操作虽然包含读取、加 1 和写入三个步骤,但由于 synchronized 的原子性保障,这三个步骤被视为一个不可分割的整体,不会被其他线程打断 。当thread1执行完同步块,执行 monitorexit 指令释放锁后,thread2才有机会获取锁并执行同步块内的代码 。通过这种方式,确保了sharedVariable的递增操作在多线程环境下的原子性,最终输出的sharedVariable值为 2000,而不会出现小于 2000 的情况 。
ThreadA和ThreadB,共享变量sharedData:
public class VisibilityDemo { private static int sharedData = 0; private static final Object lock = new Object(); public static void main(String[] args) { Thread threadA = new Thread(() -> { synchronized (lock) { sharedData = 100; System.out.println("ThreadA 修改了 sharedData 为: " + sharedData); } }); Thread threadB = new Thread(() -> { synchronized (lock) { System.out.println("ThreadB 读取到的 sharedData 为: " + sharedData); } }); threadA.start(); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } threadB.start(); } }
在上述代码中,
ThreadA线程在同步块内修改了sharedData的值为 100,当它退出同步块时,通过内存屏障将修改后的值刷新到主内存 。ThreadB线程在进入同步块时,通过内存屏障从主内存中读取到ThreadA修改后的sharedData值,从而保证了可见性 。运行结果中,ThreadB读取到的sharedData值为 100,而不会是旧值 0 。
a = 1; b = 2;这样的代码,编译器和处理器可能会对这两条指令进行重排,但由于最终的结果是a为 1,b为 2,不会影响程序的正确性 。
以如下代码示例说明:
public class OrderlinessDemo { private static int x = 0, y = 0; private static final Object lock = new Object(); public static void main(String[] args) { Thread thread1 = new Thread(() -> { synchronized (lock) { x = 1; y = 2; } }); Thread thread2 = new Thread(() -> { synchronized (lock) { System.out.println("y = " + y + ", x = " + x); } }); thread1.start(); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } thread2.start(); } }
在上述代码中,
thread1线程在同步块内先对x赋值为 1,再对y赋值为 2 。由于 synchronized 的有序性保障,thread2线程在进入同步块时,一定能看到thread1线程按照顺序执行后的结果,即y为 2,x为 1 。运行结果中,thread2输出的y和x的值一定是 2 和 1,而不会出现y为 0,x为 1 或者其他不符合顺序的情况 。
int a = sharedVar1; // 读操作1
// LoadLoad屏障
int b = sharedVar2; // 读操作2
假设sharedVar1和sharedVar2是共享变量,并且读操作 2 依赖于读操作 1 的结果或者需要确保读操作 1 先完成,那么在这两个读操作之间插入 LoadLoad 屏障就显得尤为重要 。LoadLoad 屏障会强制处理器检查缓存状态(基于 MESI 协议),如果发现缓存行处于 Invalid 状态,则重新从内存或其他处理器的缓存中加载最新数据,以确保读操作 2 读取的是最新值,防止了编译器和处理器对这两个读操作进行重排序 。
StoreStore 屏障:该屏障的作用是确保屏障之前的写操作(Store)先于屏障之后的写操作完成,并且屏障之前的写操作结果对其他处理器可见(即刷新到主内存) 。例如:
sharedVar1 = 10; // 写操作1
// StoreStore屏障
sharedVar2 = 20; // 写操作2
在这个例子中,如果写操作 2 依赖于写操作 1 的结果,或者需要确保写操作 1 的结果对后续写操作可见,就需要插入 StoreStore 屏障 。它会强制将写缓冲区(Store Buffer)中的数据刷新到缓存(或主内存),确保其他处理器能看到写操作 1 的结果,从而防止写操作被重排序 。
LoadStore 屏障:LoadStore 屏障的作用是确保屏障之前的读操作(Load)先于屏障之后的写操作(Store)完成 。例如:
int value = sharedVar; // 读操作
// LoadStore屏障
anotherVar = value + 1; // 写操作,依赖于读操作的结果
当读操作后面跟着一个依赖于读操作结果的写操作时,插入 LoadStore 屏障是必要的 。它会阻止处理器将写操作重排序到读操作之前,同时确保读操作完成后再执行写操作,避免使用过期的数据执行写操作 。
StoreLoad 屏障:这是四种内存屏障中最为严格的一种,它确保屏障之前的所有写操作(Store)完成并对其他处理器可见后,才执行屏障之后的读操作(Load) 。例如:
sharedVar = 30; // 写操作
// StoreLoad屏障
int result = anotherVar; // 读操作
常见于 volatile 变量的写操作之后,或者锁释放(unlock)时 。它会强制将写缓冲区(Store Buffer)中的数据全部刷新到主内存,并让当前处理器丢弃缓存中失效的数据(即重新从主内存加载),从而确保读操作读取的是最新值 。由于需要刷新整个写缓冲区并可能使缓存失效,StoreLoad 屏障的开销是四种屏障中最大的 。
这四种内存屏障通过禁止重排序和强制刷新缓存 / 写缓冲区,有效地解决了多核 CPU 的可见性与有序性问题 。在 Java 中,volatile 变量的读写以及 synchronized 关键字的加锁解锁操作,都会自动插入相应的内存屏障,以保证多线程编程的正确性 。
public class SynchronizedMemoryBarrierDemo { private static int sharedData = 0; private static final Object lock = new Object(); public static void main(String[] args) { Thread threadA = new Thread(() -> { synchronized (lock) { sharedData = 100; System.out.println("ThreadA 修改了 sharedData 为: " + sharedData); } }); Thread threadB = new Thread(() -> { synchronized (lock) { System.out.println("ThreadB 读取到的 sharedData 为: " + sharedData); } }); threadA.start(); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } threadB.start(); } }
当
threadA线程进入synchronized(lock)同步块时,会插入 LoadLoad 屏障和 LoadStore 屏障 。LoadLoad 屏障确保在读取共享变量sharedData之前,先读取主内存中的最新值,而不是从工作内存中读取旧值,避免了读取到过期数据 。LoadStore 屏障则确保在读取共享变量sharedData之后,对其他共享变量的写入操作不会被重排序到读取之前,保证了读操作和后续写操作的顺序性 。
在threadA线程退出同步块时,会插入 StoreStore 屏障和 StoreLoad 屏障 。StoreStore 屏障确保在写入共享变量sharedData之后,之前对其他共享变量的写入操作都已经完成,保证了写操作的顺序性 。StoreLoad 屏障则确保在写入共享变量sharedData之后,对其他共享变量的读取操作不会被重排序到写入之前,同时将修改后的值刷新到主内存,保证了其他线程能够读取到最新的值 。
当threadB线程进入同步块时,同样会插入 LoadLoad 屏障和 LoadStore 屏障,确保从主内存中读取到threadA修改后的sharedData值 。通过这些内存屏障的协同工作,synchronized 保证了多线程环境下共享变量sharedData的可见性和有序性,使得threadB能够读取到threadA修改后的最新值 。在多线程编程中,正确理解和运用 synchronized 中的内存屏障机制,对于编写高效、健壮的并发程序至关重要 。
【从0到1构建一个ClaudeAgent】协作-Agent团队