在Java中,有几种常见的锁机制用于实现并发控制,包括:
synchronized(内置锁)
ReentrantLock(可重入锁)
ReadWriteLock(读写锁)
StampedLock(标记锁)
下面我会详细解析每种锁机制的原理、优缺点和适用场景,并给出相应的代码示例。
synchronized(内置锁):
原理:synchronized关键字是Java语言提供的原生锁机制,利用对象头的mark word来实现对对象的锁定和解锁。当多个线程尝试获取同一个对象的锁时,只有一个线程能够成功,其他线程会被阻塞。它还提供了可见性和原子性保证。
优点:public class SynchronizedExample { private final Object lock = new Object(); public void synchronizedMethod() { synchronized (lock) { // 需要同步的代码 } }}
使用简单,直接在需要同步的代码块上加上synchronized关键字即可。
内置的重量级锁在锁竞争激烈的情况下表现良好。
缺点:锁的获取和释放是隐式的,没有显示的方式来控制它们。
不能中断一个正在等待获取锁的线程。
适用场景:对于简单的并发场景,synchronized通常是最好的选择。ReentrantLock(可重入锁):
原理:ReentrantLock是JDK提供的重入锁实现,它使用了与synchronized相似的原理,但提供了更多的灵活性和功能。它使用CAS(Compare And Swap)操作实现对锁的获取和释放,可以通过lock()和unlock()方法手动控制锁的获取和释放。
优点:import java.util.concurrent.locks.ReentrantLock;public class ReentrantLockExample { private final ReentrantLock lock = new ReentrantLock(); public void reentrantLockMethod() { lock.lock(); try { // 需要同步的代码 } finally { lock.unlock(); } }}
提供了显示的获取和释放锁的方式,可以更灵活地控制锁的范围和生命周期。
支持公平锁和非公平锁,可以通过构造函数指定。
支持可重入性,同一个线程可以多次获取锁而不会造成死锁。
缺点:使用相对复杂,需要手动控制锁的获取和释放。
ReentrantLock比synchronized更消耗系统资源。
适用场景:在需要更精细地控制锁的获取和释放、可重入性和公平性的情况下,ReentrantLock是一个不错的选择。ReadWriteLock(读写锁):
原理:ReadWriteLock是JDK提供的读写分离锁实现。它允许多个线程同时读取共享数据,但在写操作时需要互斥访问。ReadWriteLock通过将读操作和写操作分离,提高了并发读取的性能。
优点:在读多写少的场景下,比重入锁性能更好。
可以提供更高的并发访问性能,读操作可以同时进行,写操作需要互斥。
缺点:写锁会阻塞所有的读操作和其他写操作,可能导致写操作的饥饿。
适用场景:在读多写少的情况下,使用ReadWriteLock可以提高并发读取性能。
import java.util.concurrent.locks.ReentrantReadWriteLock;public class ReadWriteLockExample { private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); public void readMethod() { lock.readLock().lock(); try { // 读取共享数据 } finally { lock.readLock().unlock(); } } public void writeMethod() { lock.writeLock().lock(); try { // 写入共享数据 } finally { lock.writeLock().unlock(); } }}
4.StampedLock(标记锁):
原理:StampedLock是JDK提供的一种乐观读锁机制。它将锁的状态表示为一个stamp标记,每次读写操作都会返回一个stamp。读操作可以随时进行,写操作需要对标记进行检查,如果标记已经改变则需要重试。StampedLock支持降级为读锁和升级为写锁。
优点:
支持更高的并发性和读操作的灵活性。
可以实现乐观读,避免阻塞其他读操作。
缺点:写锁的获取会阻塞所有的读操作和写操作。
应用需要额外注意写锁的可重入性和可中断性。
适用场景:在读多写少且读操作较频繁的情况下,并且对读操作的性能有较高要求时,可以考虑使用StampedLock。
import java.util.concurrent.locks.StampedLock;public class StampedLockExample { private final StampedLock lock = new StampedLock(); private int value; public void readMethod() { long stamp = lock.tryOptimisticRead(); int localValue = value; if (!lock.validate(stamp)) { stamp = lock.readLock(); try { localValue = value; } finally { lock.unlockRead(stamp); } } // 使用localValue进行读操作 } public void writeMethod() { long stamp = lock.writeLock(); try { // 写入共享数据 } finally { lock.unlockWrite(stamp); } }}
上面是关于Java并发中常见的锁机制的详细解析和示例代码。不同的锁机制适用于不同的并发场景,在选择使用时需要根据具体需求和性能要求进行评估和决策。
而Atomic类是Java提供的一组原子操作类,可以保证特定操作的原子性。与锁机制相比,Atomic类提供了一种更轻量级的方式来实现对共享变量的同步访问。
性能方面,Atomic类通常比锁机制要更加高效。锁机制涉及到线程的上下文切换、阻塞和唤醒等开销,而Atomic类使用了CAS(Compare and Swap)操作,通过硬件指令级别的原子操作,避免了上下文切换和阻塞。
Atomic类适用于以下场景:
需要对单个变量进行原子操作的场景,例如计数器、ID生成器等。
读操作非常频繁,写操作较少且对实时性要求不高的场景。
不需要额外的等待和阻塞机制,即不需要对线程进行调度管理。
需要注意的是,Atomic类能够提供的功能相对有限,只能对单个变量进行原子操作。如果需要对多个变量或复杂的操作进行同步控制,还是需要使用锁机制。此外,Atomic类对于高并发场景下的竞争会导致CAS操作的自旋次数增加,可能对性能产生一定的影响。
因此,在实际应用中,需要根据具体的需求和性能要求来选择合适的同步机制,可以根据场景将Atomic类与锁机制结合使用,以达到更好的性能和效果。