synchronize与reentrantlock区别
Synchronize与ReentrantLock的核心区别
一、锁的特性差异
1. 锁的实现机制
synchronized是Java中的关键字,由JVM底层实现,属于内置锁。它通过对象头中的Mark Word来存储锁信息,在编译阶段会自动生成锁的获取和释放指令,开发者无需手动释放锁,当代码执行完同步代码块或方法,或者发生异常时,JVM会自动释放锁。
ReentrantLock是JDK层面实现的锁,属于java.util.concurrent.locks包下的类,基于AQS(AbstractQueuedSynchronizer)框架实现。它需要开发者手动调用lock()方法获取锁,unlock()方法释放锁,且通常建议将unlock()放在finally块中,避免因异常导致锁无法释放。
2. 可重入性
两者都支持可重入锁,即同一个线程可以多次获取同一把锁,不会出现自己阻塞自己的情况。
对于synchronized,JVM会记录线程的锁持有次数,每进入一次同步块次数加1,退出一次次数减1,当次数为0时释放锁。
ReentrantLock则通过AQS中的state变量来计数,每次lock()调用会将state加1,unlock()调用将state减1,state为0时锁被释放。
3. 公平性
synchronized默认是不公平锁,JVM在调度锁时,会优先唤醒等待时间较短的线程,或者直接让新线程获取锁,不保证等待队列中线程的获取顺序,这样可以减少线程切换带来的开销,但可能导致部分线程长期无法获取锁,出现“饥饿”现象。
ReentrantLock支持公平锁和非公平锁两种模式,在创建对象时可以通过构造参数指定:new ReentrantLock(true)表示公平锁,new ReentrantLock(false)或默认构造方法表示非公平锁。公平锁会严格按照线程的等待顺序来分配锁,先等待的线程优先获取锁,避免线程饥饿,但会带来一定的性能开销。
二、锁的使用灵活性差异
1. 锁的获取与释放
synchronized的锁获取和释放是自动的,开发者只能在同步代码块或同步方法中使用,无法在代码块中间手动释放锁,也无法尝试获取锁后非阻塞地退出。
ReentrantLock提供了更灵活的锁操作:除了基础的lock()方法,还支持tryLock()方法,该方法会尝试获取锁,如果获取成功返回true,失败则立即返回false,不会阻塞线程;还可以使用tryLock(long timeout, TimeUnit unit)方法,在指定时间内尝试获取锁,超时后自动放弃,这对于处理一些需要限时等待的场景非常有用。此外,开发者可以在合适的位置手动调用unlock()释放锁,无需等到代码块执行完毕。
2. 中断响应
synchronized不支持中断,当线程等待synchronized锁时,无法通过interrupt()方法中断线程的等待状态,线程会一直阻塞直到获取锁。
ReentrantLock支持中断响应,提供了lockInterruptibly()方法,线程在调用该方法获取锁时,如果其他线程调用了该线程的interrupt()方法,该线程会抛出InterruptedException并终止等待,从而避免线程无限期阻塞,这在处理一些需要及时响应中断的业务场景中很重要。
3. 条件锁支持
synchronized结合wait()、notify()、notifyAll()方法可以实现线程间的通信,但一个synchronized锁只能对应一个等待队列,notify()方法只能随机唤醒一个等待线程,notifyAll()会唤醒所有等待线程,灵活性较差。
ReentrantLock可以通过newCondition()方法创建多个Condition对象,每个Condition对应一个等待队列,开发者可以通过await()、signal()、signalAll()方法针对特定队列的线程进行唤醒和等待,实现更精细的线程通信控制,比如在生产者-消费者模型中,可以分别为生产者和消费者创建独立的Condition队列,精准唤醒目标线程。
三、性能与适用场景差异
1. 性能表现
在Java早期版本中,synchronized的性能较差,因为锁的竞争和释放会导致大量的线程上下文切换。但从Java 6开始,JVM对synchronized进行了优化,引入了偏向锁、轻量级锁、重量级锁等锁升级机制,在低并发场景下,synchronized的性能已经和ReentrantLock相差无几,甚至在某些场景下更优,因为JVM可以针对内置锁进行更深度的优化。
在高并发场景下,ReentrantLock的性能表现通常更稳定,因为它提供了更灵活的锁控制策略,比如可以避免notifyAll()导致的大量线程唤醒开销,通过Condition精准唤醒目标线程。
2. 适用场景
synchronized适用于大多数简单的同步场景,比如基础的线程安全控制,因为它使用简单,无需手动管理锁的释放,降低了开发者出错的概率,而且JVM的自动优化能保证较好的性能。
ReentrantLock适用于复杂的同步场景,比如需要实现公平锁、中断响应、限时锁等待、多条件队列线程通信等需求的场景,例如在一些高并发的业务系统中,需要更精细的线程调度和控制时,ReentrantLock是更好的选择。
四、代码可读性与维护性
synchronized的代码结构更简洁,同步代码块或同步方法一目了然,其他开发者可以快速理解代码的同步逻辑,维护成本较低。
ReentrantLock需要手动管理锁的获取和释放,代码相对繁琐,尤其是需要将unlock()放在finally块中,增加了代码的复杂度,如果开发者遗漏unlock()调用,会导致锁无法释放,引发死锁问题,因此对开发者的代码规范要求更高,维护成本也相对较高。





