Synchronized是在并发编程中很经常使用,Java中除了提供Lock等API来实现互斥以外,还提供了语法(关键字)层面的Synchronized来实现互斥同步原语,今天闲来聊聊Synchronized关键字。
Synchronized可以修饰代码块、方法或类。在多线程环境下,Synchronized能够确保同一时间只有一个线程访问被保护的代码块,从而避免了数据竞争和不一致的问题。
对象锁是指对实例对象进行加锁,使得同一时间只有一个线程可以执行被保护的代码块。每个对象都有一个与之关联的监视器锁(Monitor Lock),也称为内部锁(Intrinsic Lock)或互斥锁(Mutex Lock)。只有获得了对象的锁,才能进入同步代码块执行。
示例代码:
public class MyClass {
public void synchronizedMethod() {
synchronized (this) {
// 同步代码块
// ...
}
}
}synchronized关键字修饰的synchronizedMethod方法是一个同步方法,它的对象锁就是MyClass的实例对象。当多个线程调用这个方法时,只有一个线程能够获得对象锁,进入同步代码块执行,其他线程需要等待。
类锁是指对类进行加锁,使得同一时间只有一个线程可以执行被保护的代码块。类锁是基于类对象而不是实例对象的,因此它在类的所有实例之间是共享的。
示例代码:
public class MyClass {
public static synchronized void synchronizedMethod() {
// 同步代码块
// ...
}
}synchronized关键字修饰的synchronizedMethod方法是一个静态同步方法,它的类锁是MyClass类对象。因为类锁是在类级别上的,所以即使有多个MyClass的实例,它们共享同一个类锁。
方法锁是指对整个方法进行加锁,使得同一时间只有一个线程可以执行该方法。方法锁实际上是对象锁的一种特殊形式,它锁定的是该方法所属对象的实例。
示例代码:
public class MyClass {
public synchronized void synchronizedMethod() {
// 同步代码块
// ...
}
}synchronized关键字修饰的synchronizedMethod方法是一个实例方法,它的方法锁就是该方法所属对象的实例。因此,同一时间只有一个线程能够执行该方法。
我们先来写一段不带synchronized代码:
class SyncTest {
public void method1() {
method2();
}
private static void method2() {
}
}使用javac命令编译成.class文件,再通过反编译javap查看文件信息。
javap -verbose xxx.class可以看到如下信息:

加上synchronized代码后:
class SyncTest {
public void method1() {
synchronized (this) {
}
method2();
}
private static void method2() {
}
}再来javap反编译:

对比之下可以发现,加了synchronized方法多了monitorenter和monitorexit指令。
先来说说monitor对象。在Java中,每个对象都与一个Monitor对象关联。Monitor对象是用于实现Synchronized关键字的内部机制,它负责实现线程的互斥访问和等待/通知机制。每个Monitor对象都有一个锁标志和等待队列。而每个Java对象在内存中都有一个对象头,其中包含了Monitor相关的信息。
monitorenter指令用于获取对象的Monitor,即加锁操作。执行monitorenter指令时,首先会尝试获取对象的锁,如果锁可用,则线程获得锁,并继续执行后续的指令。如果锁已经被其他线程持有,则当前线程会进入阻塞状态,等待锁的释放。当线程成功获取到锁后,会将锁的计数器加1,表示当前线程持有锁的数量。
monitorexit指令用于释放对象的Monitor,即解锁操作。执行monitorexit指令时,会将锁的计数器减1。如果锁的计数器为0,表示当前线程已经释放了该锁,其他等待锁的线程可以继续竞争获取锁。如果锁的计数器仍大于0,表示当前线程仍持有锁,其他线程仍无法获取该锁。
网上借来一张图说明对象,对象监视器,同步队列以及执行线程状态之间的关系:

该图可以看出,任意线程对Object的访问,首先要获得Object的监视器,如果获取失败,该线程就进入同步状态,线程状态变为BLOCKED,当Object的监视器占有者释放后,在同步队列中得线程就会有机会重新获取该监视器。
当一个线程要执行被Synchronized修饰的同步代码块时,它需要先获得对象的监视器锁(Monitor Lock)。如果对象锁被其他线程占用,则该线程会进入阻塞状态,直到获取到对象锁后才能执行同步代码块。对象锁的实现是基于对象头中的标记位实现的,它通过CAS(Compare and Swap)操作来保证线程的互斥访问。
类锁的实现与对象锁类似,不同之处在于类锁是基于类对象而不是实例对象的。类对象在JVM中只有一份,所以类锁在整个类的所有实例之间是共享的。类锁的实现同样是基于对象头中的标记位实现的。
方法锁实际上就是实例对象锁或类对象锁,其原理与对象锁和类锁相同。方法锁的加锁粒度是整个方法,即在进入方法前获取锁,在方法执行完毕后释放锁。
通过合理使用Synchronized,我们可以有效地保护共享资源,避免数据竞争和不一致的问题。同时,我们也要注意锁的粒度、死锁的避免以及性能与安全的平衡,以提供高效可靠的多线程程序。