显式锁Lock和内置锁知识整理
文章目录
- (一)基础认识
- (1)synchronized内置锁的不足之处
- (2)锁的几种分类
- 可重入锁和非可重入锁
- 公平锁与非公平锁
- 读写锁和排它锁
- (二)Lock
- (1)ReentrantLock
- (2)轮询锁与定时锁
- (3)可中断的锁获取操作
- (4)非块结构的加锁
- (5)ReentrantReadWriteLock
- (6)Condition的使用以及和Object的监视器比较
- (7)synchronized和ReentrantLock的选择
- 参考
(一)基础认识
(1)synchronized内置锁的不足之处
【1】synchronized内置锁无法中断一个正在等待获取锁的线程
【2】synchronized无法知道线程有没有成功获取到锁
【3】如果临界区是只读操作,其实可以多线程一起执行,但使用synchronized的话,同一时间只能有一个线程执行。
【4】synchronized不支持可轮询的、定时的以及中断的锁获取操作
(2)锁的几种分类
1. 可重入锁和非可重入锁
所谓重入锁,顾名思义。就是支持重新进入的锁,也就是说这个锁支持一个线程对资源重复加锁。
简单理解:线程A进去一个加锁的方法method1,此时方法内部有调用了加锁的方法method2,它可以重新进入这个锁,不会出现任何异常。
2. 公平锁与非公平锁
这里的“公平”,其实通俗意义来说就是“先来后到”,也就是FIFO。如果对一个锁来说,先对锁获取请求的线程一定会先被满足,后对锁获取请求的线程后被满足,那这个锁就是公平的。反之,那就是不公平的。
一般情况下,非公平锁能提升一定的效率。但是非公平锁可能会发生线程饥饿(有一些线程长时间得不到锁)的情况。所以要根据实际的需求来选择非公平锁和公平锁。
JDK提供的锁默认都是使用非公平锁,选择公平锁需要选择设置。
3. 读写锁和排它锁
synchronized用的锁和ReentrantLock,其实都是“排它锁”。也就是说,这些锁在同一时刻只允许一个线程进行访问。
而读写锁可以再同一时刻允许多个读线程访问。Java提供了ReentrantReadWriteLock类作为读写锁的默认实现,内部维护了两个锁:一个读锁,一个写锁。通过分离读锁和写锁,使得在“读多写少”的环境下,大大地提高了性能。
注意,即使用读写锁,在写线程访问时,所有的读线程和其它写线程均被阻塞。
(二)Lock
(1)ReentrantLock
ReentrantLock是一个非抽象类,它是Lock接口的JDK默认实现,实现了锁的基本功能。从名字上看,它是一个”可重入“锁,从源码上看,它内部有一个抽象类Sync,是继承了AQS,自己实现的一个同步器。同时,ReentrantLock内部有两个非抽象类NonfairSync和FairSync,它们都继承了Sync。从名字上看得出,分别是”非公平同步器“和”公平同步器“的意思。这意味着ReentrantLock可以支持”公平锁“和”非公平锁“。默认是非公平锁。
(2)轮询锁与定时锁
可定时的与轮询的锁获取模式是由tryLock方法实现的,与无条件的锁获取模式相比,它具有更完善的错误恢复机制。
(3)可中断的锁获取操作
lockInterruptibly方法能够在获得锁的同时保持对中断的响应,并且由于它包含在lock中,因此无需创建其他类型的不可中断阻塞机制。
当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就是说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
Lock lock =new ReentrantLock();
public void test1() throws InterruptedException {
lock.lockInterruptibly();
try {
//.....
}
finally {
lock.unlock();
}
}
【注意】
1、由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException。
2、当一个线程获取了锁之后,是不会被interrupt()方法中断的。单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。
3、 而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。这就是lock相对于synchronized可中断的锁获取操作的特性不同之处。
(4)非块结构的加锁
使用synchronized锁时,锁的获取和释放操作都是基于代码块的,释放锁的操作总是与获取锁的操作处于同一个代码块中,而不考虑控制权如何退出该代码块。自动的所释放操作简化了对程序的分析,避免了可能的编码错误,但有时候需要更灵活的加锁规则。
例如类似于ConcurrentHashMap锁分段技术,通过类似的原则来降低链表中锁的粒度,即为每一个链表节点使用一个独立的锁,是不同的线程能独立地对链表的不同部分进行操作。每次的加锁释放锁操作都是只针对于特定节点,这样的方式会在并发操作时效能更好。
(5)ReentrantReadWriteLock
这个类也是一个非抽象类,它是ReadWriteLock接口的JDK默认实现。它与ReentrantLock的功能类似,同样是可重入的,支持非公平锁和公平锁。不同的是,它还支持”读写锁“。
(6)Condition的使用以及和Object的监视器比较
每个对象都可以用继承自Object的wait/notify方法来实现等待/通知机制。而Condition接口也提供了类似Object监视器的方法,通过与Lock配合来实现等待/通知模式。
那为什么既然有Object的监视器方法了,还要用Condition呢?这里有一个二者简单的对比:
摘录:《深入浅出Java多线程》
Condition和Object的wait/notify基本相似。其中,Condition的await方法对应的是Object的wait方法,而Condition的signal/signalAll方法则对应Object的notify/notifyAll()。但Condition类似于Object的等待/通知机制的加强版。我们来看看主要的方法:
(7)synchronized和ReentrantLock的选择
1:功能层面上,ReentrantLock提供了内置锁synchronized更多的扩展,包括定时的加锁,可中断的锁等待,公平性,非块结构的加锁
2:Java5及以前的版本ReentrantLock的性能是远超synchronized,但是在Java6开始synchronized进行了优化,比如对线程封闭的锁对象的锁消除优化,通过增加锁的粒度来消除内置锁的同步等等方式。Java6之后的版本ReentrantLock的性能略有胜出。
3:与显示锁相比,内置锁仍然具有很大的优势。内置锁开发人员更加熟悉且使用起来更加简单方便。不建议在系统中混合式的使用内置锁和显示锁。Lock锁的危险性比同步机制要高,如果忘记在finally块中调用unlock,那么虽然代码正常运行,但是如果发生问题将很难排查。
4:在Java6之后,仅当内置锁无法满足需求,或者相较于内置锁当前的业务场景更加适用于显示锁,否则更加建议使用内置锁来完成工作。
5:Java未来更可能提升synchronized而不是ReentrantLock的性能,因为synchronized是JVM的内置属性。
参考
《深入浅出Java多线程》
《Java并发编程实战》
还没有评论,来说两句吧...