目录
1、前言
2、Synchronized使用
2.1、对象锁(Instance Lock)
2.2、类锁(Class Lock)
2.3、方法锁(Method Lock)
3、原理分析
3.1、monitor对象
3.2、monitorenter
3.3、monitorexit
3.4、对象锁的原理
3.5、类锁的原理
3.6、方法锁的原理
4、Synchronized的优化与改进
5、小结
1、前言
Synchronized是在并发编程中很经常使用,Java中除了提供Lock等API来实现互斥以外,还提供了语法(关键字)层面的Synchronized来实现互斥同步原语,今天闲来聊聊Synchronized关键字。
2、Synchronized使用
Synchronized可以修饰代码块、方法或类。在多线程环境下,Synchronized能够确保同一时间只有一个线程访问被保护的代码块,从而避免了数据竞争和不一致的问题。
2.1、对象锁(Instance Lock)
对象锁是指对实例对象进行加锁,使得同一时间只有一个线程可以执行被保护的代码块。每个对象都有一个与之关联的监视器锁(Monitor Lock),也称为内部锁(Intrinsic Lock)或互斥锁(Mutex Lock)。只有获得了对象的锁,才能进入同步代码块执行。
示例代码:
public class MyClass {
public void synchronizedMethod() {
synchronized (this) {
// 同步代码块
// ...
}
}
}
synchronized关键字修饰的synchronizedMethod方法是一个同步方法,它的对象锁就是MyClass的实例对象。当多个线程调用这个方法时,只有一个线程能够获得对象锁,进入同步代码块执行,其他线程需要等待。
2.2、类锁(Class Lock)
类锁是指对类进行加锁,使得同一时间只有一个线程可以执行被保护的代码块。类锁是基于类对象而不是实例对象的,因此它在类的所有实例之间是共享的。
示例代码:
public class MyClass {
public static synchronized void synchronizedMethod() {
// 同步代码块
// ...
}
}
synchronized关键字修饰的synchronizedMethod方法是一个静态同步方法,它的类锁是MyClass类对象。因为类锁是在类级别上的,所以即使有多个MyClass的实例,它们共享同一个类锁。
2.3、方法锁(Method Lock)
方法锁是指对整个方法进行加锁,使得同一时间只有一个线程可以执行该方法。方法锁实际上是对象锁的一种特殊形式,它锁定的是该方法所属对象的实例。
示例代码:
public class MyClass {
public synchronized void synchronizedMethod() {
// 同步代码块
// ...
}
}
synchronized关键字修饰的synchronizedMethod方法是一个实例方法,它的方法锁就是该方法所属对象的实例。因此,同一时间只有一个线程能够执行该方法。
3、原理分析
我们先来写一段不带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指令。
3.1、monitor对象
先来说说monitor对象。在Java中,每个对象都与一个Monitor对象关联。Monitor对象是用于实现Synchronized关键字的内部机制,它负责实现线程的互斥访问和等待/通知机制。每个Monitor对象都有一个锁标志和等待队列。而每个Java对象在内存中都有一个对象头,其中包含了Monitor相关的信息。
- 工作原理:
- 当线程执行到进入Synchronized块或方法时,会尝试获取对象的Monitor锁。
- 如果Monitor锁可用,线程获得锁并继续执行,同时锁标志设置为被当前线程持有。
- 如果Monitor锁已经被其他线程持有,线程进入阻塞状态,并被放入等待队列。
- 当持有锁的线程执行完Synchronized块或方法,并调用monitorexit指令释放锁时,等待队列中的一个线程被唤醒,然后竞争锁。
- 线程获取到锁后,锁标志设置为被该线程持有,其他等待线程继续等待或进入就绪状态。
- 等待/通知机制:
- 在Monitor对象上等待的线程通过wait()方法进入等待状态,释放锁,并加入到等待队列中。
- 调用notify()方法可以唤醒等待队列中的一个线程,使其进入就绪状态。
- 调用notifyAll()方法可以唤醒等待队列中的所有线程,使它们进入就绪状态。
- 唤醒的线程会尝试重新获得锁,一旦获得锁,就可以继续执行。
- 锁升级:
- 偏向锁(Biased Locking):当一个线程获取到锁时,Monitor会记录该线程的Thread ID,以后该线程再次获取锁时就无需竞争。
- 轻量级锁(Lightweight Locking):当多个线程争用同一个锁时,锁会升级为轻量级锁,使用CAS(Compare and Swap)操作来加锁和解锁。
- 重量级锁(Heavyweight Locking):当轻量级锁获取锁的过程中发生竞争,锁会升级为重量级锁,使用互斥量来实现锁的获取和释放。
3.2、monitorenter
monitorenter指令用于获取对象的Monitor,即加锁操作。执行monitorenter指令时,首先会尝试获取对象的锁,如果锁可用,则线程获得锁,并继续执行后续的指令。如果锁已经被其他线程持有,则当前线程会进入阻塞状态,等待锁的释放。当线程成功获取到锁后,会将锁的计数器加1,表示当前线程持有锁的数量。
3.3、monitorexit
monitorexit指令用于释放对象的Monitor,即解锁操作。执行monitorexit指令时,会将锁的计数器减1。如果锁的计数器为0,表示当前线程已经释放了该锁,其他等待锁的线程可以继续竞争获取锁。如果锁的计数器仍大于0,表示当前线程仍持有锁,其他线程仍无法获取该锁。
网上借来一张图说明对象,对象监视器,同步队列以及执行线程状态之间的关系:
该图可以看出,任意线程对Object的访问,首先要获得Object的监视器,如果获取失败,该线程就进入同步状态,线程状态变为BLOCKED,当Object的监视器占有者释放后,在同步队列中得线程就会有机会重新获取该监视器。
3.4、对象锁的原理
当一个线程要执行被Synchronized修饰的同步代码块时,它需要先获得对象的监视器锁(Monitor Lock)。如果对象锁被其他线程占用,则该线程会进入阻塞状态,直到获取到对象锁后才能执行同步代码块。对象锁的实现是基于对象头中的标记位实现的,它通过CAS(Compare and Swap)操作来保证线程的互斥访问。
3.5、类锁的原理
类锁的实现与对象锁类似,不同之处在于类锁是基于类对象而不是实例对象的。类对象在JVM中只有一份,所以类锁在整个类的所有实例之间是共享的。类锁的实现同样是基于对象头中的标记位实现的。
3.6、方法锁的原理
方法锁实际上就是实例对象锁或类对象锁,其原理与对象锁和类锁相同。方法锁的加锁粒度是整个方法,即在进入方法前获取锁,在方法执行完毕后释放锁。
4、Synchronized的优化与改进
- 减小锁的粒度:在并发环境下,锁的争用是造成性能瓶颈的主要原因之一。通过减小锁的粒度,将一个大的同步代码块拆分成多个小的同步代码块,可以降低锁的竞争,提高并发性能。
- 使用局部变量代替实例变量:在某些情况下,可以将实例变量赋值给局部变量,然后在同步代码块内部使用局部变量,避免对实例变量的频繁访问,从而减少对锁的竞争。
- 使用读写锁:如果某个共享资源大部分时间是读取操作,少部分时间是写入操作,可以考虑使用读写锁(ReadWriteLock)。读写锁提供了更细粒度的锁控制,允许多个线程同时读取共享数据,而对写入操作进行互斥。
- 使用并发容器:JUC提供了一些并发容器,如ConcurrentHashMap、ConcurrentLinkedQueue等,它们在实现上使用了更复杂的锁机制,能够提供更好的并发性能。如果需要在多线程环境下使用容器,可以考虑使用这些并发容器。
- 使用Lock接口:除了Synchronized关键字外,JUC还提供了Lock接口及其实现类,如ReentrantLock、ReadWriteLock等。相比于Synchronized,Lock提供了更灵活的锁控制,可以手动获取和释放锁,并支持公平锁和非公平锁的选择。
- 避免死锁:死锁是多线程编程中常见的问题,它发生在多个线程相互等待对方释放锁的情况下。为避免死锁,应尽量减少锁的嵌套使用,按照相同的顺序获取锁,避免循环等待,以及合理设计锁的粒度。
- 考虑性能与安全的平衡:在并发编程中,性能和安全是需要平衡的。过多的同步操作可能会影响性能,而过少的同步操作可能会引发线程安全问题。需要根据具体情况综合考虑,选择适当的同步策略。
5、小结
通过合理使用Synchronized,我们可以有效地保护共享资源,避免数据竞争和不一致的问题。同时,我们也要注意锁的粒度、死锁的避免以及性能与安全的平衡,以提供高效可靠的多线程程序。