synchronized
关键字和ReentrantLock
都是Java中用来实现线程同步的重要机制,它们都允许在一个多线程环境中保护临界区,确保同一时刻只有一个线程能够访问共享资源。下面我们将分别探讨这两种机制的原理及其区别。
synchronized
的关键字原理
synchronized
关键字可以修饰方法或者同步块,在JVM层面实现了互斥锁的语义。当一个线程试图访问synchronized
方法或同步块时,它会尝试获取对应的锁。一旦获取到锁,其他线程就无法再获取此锁,直到第一个线程释放锁为止。
对象锁:
- 当
synchronized
修饰一个实例方法时,锁住的是该对象的实例。 - 当
synchronized
修饰一个静态方法时,锁住的是该类的类对象锁。
- 当
监视器锁:
- 每个Java对象都可以作为锁,称为监视器锁。
- 当一个线程进入
synchronized
代码块时,会获取对应的监视器锁;当线程退出时,会释放该锁。 - 监视器锁具有可重入性,这意味着同一个线程可以多次获取同一个锁。
实现细节:
- 在JVM层面,
synchronized
关键字通过操作对象头中的Mark Word来实现。 - Mark Word中包含对象的哈希码、GC分代年龄、锁标志位等信息。
- 根据锁的状态(无锁状态、偏向锁、轻量级锁、重量级锁),Mark Word会动态改变其存储的内容以适应不同的场景。
- 在JVM层面,
ReentrantLock
的原理
ReentrantLock
是一个显式的锁,它不是通过编译器自动实现的,而是需要程序员手动地去加锁和解锁。ReentrantLock
提供了比synchronized
更多的灵活性,比如公平性和非公平性的选择、尝试锁、条件变量等。
可重入性:
ReentrantLock
同样支持可重入性,即一个已经获取锁的线程可以再次获取该锁而不阻塞。
公平性:
ReentrantLock
提供了公平锁和非公平锁的选择。公平锁按照线程请求锁的顺序来获取锁,而非公平锁则可能让后来的线程先获取锁。
尝试锁:
ReentrantLock
允许尝试获取锁而不立即阻塞。如果锁不可用,可以选择等待一定的时间后返回。
条件变量:
ReentrantLock
通过Condition
接口提供了条件变量的支持,这使得线程可以等待某个条件成立后再继续运行。
synchronized
与ReentrantLock
的区别
语法:
synchronized
不需要手动获取和释放锁,由编译器和JVM自动管理。ReentrantLock
需要程序员明确地调用lock()
和unlock()
方法来获取和释放锁。
灵活性:
synchronized
相对简单且易于使用,但提供的功能较少。ReentrantLock
提供了更高级的功能,如公平锁、非公平锁、尝试锁等。
性能:
- 在某些情况下,
ReentrantLock
可以提供更好的性能,尤其是在需要使用高级功能时。 synchronized
在JDK 1.6之后也进行了优化,例如引入了偏向锁和轻量级锁等机制,使得在某些情况下性能甚至优于ReentrantLock
。
- 在某些情况下,
异常处理:
- 使用
synchronized
时,如果线程因为异常而退出同步块,锁会自动释放。 - 使用
ReentrantLock
时,如果线程因为异常而退出,需要通过try-finally结构来确保锁被释放。
- 使用
可见性和原子性:
synchronized
不仅提供了互斥性,还隐式提供了内存可见性和有序性保证。ReentrantLock
仅提供互斥性,对于可见性和有序性需要额外的同步机制(如volatile
和happens-before
规则)。
结论
synchronized
和ReentrantLock
各有优势,选择哪一个取决于具体的使用场景。如果你需要更高级的控制能力,比如公平锁或尝试锁,那么ReentrantLock
可能是更好的选择。如果你想要简单快速地实现同步,并且不关心额外的高级特性,那么synchronized
就足够了。