在Java并发编程中,synchronized关键字是最基础也是最常用的同步手段。它能够保证在同一时刻,只有一个线程可以执行某个方法或代码块,从而解决线程安全问题。然而,很多开发者对synchronized的理解停留在"加锁"的层面,对于锁的对象是谁、不同用法有何区别等问题缺乏清晰认识。本文将从synchronized的三种应用场景入手,结合实验验证,深入剖析锁的本质,并简要探讨其底层实现原理,帮助读者彻底掌握这一关键机制。
一、并发问题与synchronized的必要性
在多线程环境中,当多个线程同时访问共享资源时,由于CPU时间片轮转和指令重排序,可能会产生数据不一致、脏读等问题。例如,对一个整型变量进行自增操作,看似简单的一行代码,在底层实际包含"读取-修改-写入"三步,若没有同步控制,就会发生线程安全问题。
synchronized通过互斥锁保证了代码块的原子性和内存可见性,是JVM内置的同步机制。它有三种主要应用形式,每种形式的锁对象各不相同,理解锁对象是正确使用synchronized的关键。
二、三种应用场景及锁对象
2.1 锁作用于实例方法
当synchronized修饰一个非静态方法时,锁对象是当前实例对象 (即this)。这意味着,同一个实例的多个同步方法之间是互斥的,但不同实例的同步方法则可以并行执行。
java
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中只有一份,因此所有对该静态同步方法的调用,无论来自哪个实例,都会竞争同一把锁。
java
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、某个成员变量、甚至自定义对象。这种方式可以将锁的粒度控制得更细,减少不必要的竞争。
java
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,则效果与实例方法锁相同。也可以传入不同的锁对象,实现更精细的控制。
三、实验验证:代码演示锁的互斥效果
为了直观展示不同锁对象的互斥关系,下面给出一个完整的验证示例。
java
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并发编程的基石,看似简单,实则蕴藏着丰富的设计智慧。通过深入理解其三种应用场景及其锁对象,并掌握底层实现原理,我们就能在开发中灵活运用,既保证线程安全,又兼顾性能。