【Java并发编程篇】Lock锁介绍、ReentrantLock与synchronized的区别
什么是 Lock 接口
JDK5 之后在 JUC 中加入了 Lock 接口,该接口中只有6个方法的声明。Lock 锁是显式锁,锁的持有与释放都必须手动编写,当前线程使用 lock() 方法与 unlock() 对临界区进行加锁与释放锁,当前线程获取到锁之后,其他线程由于无法持有锁将无法进入临界区,直到当前线程释放锁,unlock() 操作必须在 finally 代码块中,这样可以确保即使临界区执行抛出异常,线程最终也能正常释放锁。
什么是重入锁 ReentrantLock
ReentrantLock 重入锁是基于 AQS 框架并实现了 Lock 接口,支持一个线程对资源重复加锁,作用与 synchronized 锁机制相当,但比 synchronized 更加灵活,同时也支持公平锁和非公平锁。
ReentrantLock 与 synchronized 的区别
使用的区别:synchronized 是 Java 的关键字,是隐式锁,依赖于 JVM 实现,当 synchronized 方法或者代码块执行完之后,JVM 会自动让线程释放对锁的占用;ReentrantLock 依赖于 API,是显式锁,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成。在发生异常时,JVM 会自动释放 synchronized 锁,因此不会导致死锁;而 ReentrantLock 在发生异常时,如果没有主动通过 unLock() 去释放锁,则很可能造成死锁现象,这也是 unLock() 语句必须写在 finally 语句块的原因。
功能的区别:ReentrantLock 相比于 synchronzied 更加灵活, 除了拥有 synchronzied 的所有功能外,还提供了其他特性:
- ReentrantLock 可以实现公平锁,而 synchronized 不能保证公平性。
- ReentrantLock 可以知道有没有成功获取锁(tryLock),而 synchronized 不支持该功能
- ReentrantLock 可以让等待锁的线程响应中断,而使用 synchronized 时,等待的线程不能够响应中断,会一直等待下去;
- ReentrantLock 可以基于 Condition 实现多条件的等待唤醒机制,而如果使用 synchronized,则只能有一个等待队列
性能的区别:在 JDK6 以前,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时,此时 ReentrantLock 的性能要远远优于 synchronizsed。但是在 JDK6 及以后的版本,JVM 对 synchronized 进行了优化,所以两者的性能变得差不多了。
总的来说,synchronizsed 和 ReentrantLock 都是可重入锁,在使用选择上需要根据具体场景而定,大部分情况下依然建议使用 synchronized 关键字,原因之一是使用方便语义清晰,二是性能上虚拟机已为我们自动优化。如果确实需要使用到 ReentrantLock 提供的多样化特性时,我们可以选择ReentrantLock。
“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
读写锁:ReadWriteLock
ReentrantLock 某些时候有局限,如果使用 ReentrantLock,主要是为了防止线程A在写数据、线程B在读数据造成的数据不一致,但如果线程C在读数据、线程D也在读数据,由于读数据是不会改变数据内容的,所以就没有必要加锁,但如果使用了 ReentrantLock,那么还是加锁了,反而降低了程序的性能,因此诞生了读写锁 ReadWriteLock。ReadWriteLock 是一个接口,而 ReentrantReadWriteLock 是 ReadWriteLock 接口的具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和写之间才会互斥,提升了读写的性能。
参考: |