Java并发——synchronized锁
在Java并发编程中synchronized关键字是最基础也是最常用的同步手段。它能够保证在同一时刻只有一个线程可以执行某个方法或代码块从而解决线程安全问题。然而很多开发者对synchronized的理解停留在“加锁”的层面对于锁的对象是谁、不同用法有何区别等问题缺乏清晰认识。本文将从synchronized的三种应用场景入手结合实验验证深入剖析锁的本质并简要探讨其底层实现原理帮助读者彻底掌握这一关键机制。一、并发问题与synchronized的必要性在多线程环境中当多个线程同时访问共享资源时由于CPU时间片轮转和指令重排序可能会产生数据不一致、脏读等问题。例如对一个整型变量进行自增操作看似简单的一行代码在底层实际包含“读取-修改-写入”三步若没有同步控制就会发生线程安全问题。synchronized通过互斥锁保证了代码块的原子性和内存可见性是JVM内置的同步机制。它有三种主要应用形式每种形式的锁对象各不相同理解锁对象是正确使用synchronized的关键。二、三种应用场景及锁对象2.1 锁作用于实例方法当synchronized修饰一个非静态方法时锁对象是当前实例对象即this。这意味着同一个实例的多个同步方法之间是互斥的但不同实例的同步方法则可以并行执行。public class InstanceLockDemo { public synchronized void method1() { // 同步代码块锁住this System.out.println(Thread.currentThread().getName() 进入method1); try { Thread.sleep(2000); } catch (InterruptedException e) {} System.out.println(Thread.currentThread().getName() 离开method1); } public synchronized void method2() { System.out.println(Thread.currentThread().getName() 进入method2); try { Thread.sleep(2000); } catch (InterruptedException e) {} System.out.println(Thread.currentThread().getName() 离开method2); } }验证实验创建同一个实例的两个线程一个调用method1另一个调用method2。结果会发现当method1持有时method2必须等待直至method1释放锁。反之如果创建两个不同的实例则两个方法可以同时执行因为它们锁的是不同的对象。2.2 锁作用于静态方法当synchronized修饰一个静态方法时锁对象是当前类的Class对象。由于Class对象在JVM中只有一份因此所有对该静态同步方法的调用无论来自哪个实例都会竞争同一把锁。public class StaticLockDemo { public static synchronized void staticMethod() { System.out.println(Thread.currentThread().getName() 进入静态同步方法); try { Thread.sleep(2000); } catch (InterruptedException e) {} System.out.println(Thread.currentThread().getName() 离开静态同步方法); } }验证实验创建两个不同的实例分别启动两个线程调用staticMethod。你会发现即使实例不同第二个线程也必须等待第一个线程执行完毕。这是因为两个线程竞争的是StaticLockDemo.class这一全局锁。2.3 锁作用于同步代码块同步代码块是最灵活的方式允许我们显式指定任意对象作为锁。锁对象可以是this、某个成员变量、甚至自定义对象。这种方式可以将锁的粒度控制得更细减少不必要的竞争。public class BlockLockDemo { private final Object lock new Object(); // 自定义锁对象 private int count 0; public void increment() { synchronized (lock) { // 锁住lock对象 count; } } public void decrement() { synchronized (lock) { count--; } } }验证实验使用同一个BlockLockDemo实例让多个线程同时调用increment和decrement它们会因竞争lock对象而互斥。如果将锁对象改为this则效果与实例方法锁相同。也可以传入不同的锁对象实现更精细的控制。三、实验验证代码演示锁的互斥效果为了直观展示不同锁对象的互斥关系下面给出一个完整的验证示例。public class SynchronizedDemo { // 实例方法锁this public synchronized void instanceMethod() { System.out.println(Thread.currentThread().getName() 进入 instanceMethod); sleep(2000); System.out.println(Thread.currentThread().getName() 离开 instanceMethod); } // 静态方法锁Class对象 public static synchronized void staticMethod() { System.out.println(Thread.currentThread().getName() 进入 staticMethod); sleep(2000); System.out.println(Thread.currentThread().getName() 离开 staticMethod); } // 同步代码块自定义锁对象 private final Object customLock new Object(); public void customBlock() { synchronized (customLock) { System.out.println(Thread.currentThread().getName() 进入 customBlock); sleep(2000); System.out.println(Thread.currentThread().getName() 离开 customBlock); } } private static void sleep(long ms) { try { Thread.sleep(ms); } catch (InterruptedException e) {} } public static void main(String[] args) { SynchronizedDemo demo1 new SynchronizedDemo(); SynchronizedDemo demo2 new SynchronizedDemo(); // 测试1同一实例的两个实例方法互斥 new Thread(() - demo1.instanceMethod(), t1).start(); new Thread(() - demo1.instanceMethod(), t2).start(); // 测试2不同实例的实例方法不互斥注释后观察 // new Thread(() - demo1.instanceMethod(), t1).start(); // new Thread(() - demo2.instanceMethod(), t2).start(); // 测试3静态方法不同实例互斥 // new Thread(() - SynchronizedDemo.staticMethod(), t3).start(); // new Thread(() - SynchronizedDemo.staticMethod(), t4).start(); // 测试4自定义锁对象同一实例互斥 // new Thread(() - demo1.customBlock(), t5).start(); // new Thread(() - demo1.customBlock(), t6).start(); } }运行上述代码可以清晰地看到锁的作用范围。建议读者动手运行体会不同场景下的阻塞现象。四、深入分析synchronized的底层原理理解锁对象之后我们有必要了解一下synchronized在JVM层面是如何实现的。这有助于我们写出更高效、更可靠的代码。4.1 对象头与Mark Word每个Java对象都有一个对象头Object Header其中包含一个Mark Word字段它记录了对象的哈希码、分代年龄以及锁状态信息。锁状态包括无锁、偏向锁、轻量级锁、重量级锁。当线程获取锁时本质上就是在修改Mark Word中的锁标志位和指向锁记录的指针。偏向锁针对单线程重复获取同一锁的场景通过CAS将线程ID写入Mark Word后续无需同步操作。轻量级锁当有竞争时偏向锁升级为轻量级锁通过自旋CAS尝试获取锁避免线程阻塞。重量级锁当竞争加剧自旋超过一定次数升级为重量级锁线程进入阻塞队列由操作系统调度。synchronized正是通过这三级锁的状态转换在无竞争时降低开销在有竞争时保证正确性。4.2 字节码层面使用javap -c反编译包含synchronized的类文件可以看到同步方法在方法上添加了ACC_SYNCHRONIZED标志JVM通过该标志隐式获取锁。同步代码块在字节码中插入monitorenter和monitorexit指令分别对应加锁和释放锁。4.3 内存语义synchronized保证了原子性和可见性。进入同步块时线程会清空工作内存中的变量副本重新从主内存中读取退出同步块时会将修改后的变量刷新到主内存。这相当于实现了lock和unlock操作保证了多线程对共享变量的可见性。五、总结与最佳实践明确锁对象实例方法锁的是this静态方法锁的是Class对象同步代码块锁的是任意指定对象。错误地选择锁对象如用String常量或Integer值可能导致意想不到的全局锁或锁失效。控制锁粒度同步代码块提供了最细粒度的控制应尽量缩小同步范围避免在锁内执行耗时操作或IO操作。锁对象不可变锁对象一旦确定不应被重新赋值否则会导致锁失效。通常使用private final Object lock new Object()作为专用锁。注意死锁当多个锁嵌套使用时要避免循环等待可以通过统一顺序或使用tryLock等高级工具规避。了解锁升级在JDK 1.6之后synchronized引入了锁升级机制性能大幅提升。但在高并发场景下频繁的锁竞争依然会导致重量级锁的膨胀此时可考虑使用ReentrantLock或ConcurrentHashMap等并发工具。synchronized作为Java并发编程的基石看似简单实则蕴藏着丰富的设计智慧。通过深入理解其三种应用场景及其锁对象并掌握底层实现原理我们就能在开发中灵活运用既保证线程安全又兼顾性能。