写在前面:上两篇我们花费了非常大的力气讲解了Java的两大关键字synchronized
和volatile
,学习了Java的锁机制和内存可见性,对并发安全有一定的知识基础,本篇就讲解在Java中如何使用这些锁,在下一篇会专门讲Java各种锁的底层实现
Lock接口
Lock的使用
lock()
:阻塞式地获取锁lockInterruptbly()
:可中断式的获取锁boolean tryLock()
:非阻塞式的获取锁,获取成功返回true,否则返回falseboolean tryLock(long, TimeUnit)
:在限定时间内非阻塞式地获取锁,如果超时或者被中断则返回false,获取成功返回trueunlock()
:释放锁newCondition()
:获取等待/通知组件,该组件与当前的锁绑定,当前线程只有获取了锁,才可以调用该组件里的wait()方法,而调用后,当前线程会释放锁(稍后会详细讲解)
1 | public static void main(String[] args) { |
tryLock()是非阻塞的,所以获取锁失败后会返回false,上面的代码运行后会得到下面的结果:
与synchroinzed对比的优势
- 非阻塞式获取锁
当前线程尝试获取锁,如果这一时刻没有其它线程获取到,则成功获取并持有锁
- 支持中断式获取锁
获取到锁的线程可以响应中断,中断异常抛出时会自动释放锁资源,synchronized无法做到这一点
- 超时获取锁
在指定时间内尝试获取锁,截止时间到了以后如果没有获取到锁则会自动返回,不会一直阻塞
使用注意事项
- 不要尝试在
try{}
内部进行加锁
如果在try{}
内部调用lock()
方法,如果发生异常加锁失败,但是finally
也会执行unlock()
,因为没有加锁而抛出异常。
Lock使用总结
- Lock接口可实现非阻塞获取锁
- Lock接口必须显式调用
lock()
和unlock()
进行加锁和写锁 - 可以使用
tryLock()
给尝试获取锁的时间加上期限
队列同步器AQS
理解锁和同步器的关系
- 同步器是实现锁的关键
- 锁是面向使用者的,同步器是面向锁的实现者的,实现者可以根据自身需求不同自定义锁的实现方式,例如共享锁、排它锁等等。
AQS中的模板方法
访问和修改同步状态
getState()
:获取当前同步状态setState()
:设置当前同步状态compareAndSetState()
:CAS设置当前状态,保证此次操作的原子性
独占式获取和释放同步状态
boolean tryAcquire()
:独占式获取同步状态boolean tryRelease()
:独占式释放同步状态
共享式获取同步状态
int tryAcquireShared(int)
:共享式获取同步状态,返回值大于0则表示获取成功boolean tryReleaseShared(int)
:共享式释放同步状态,返回值为true表示释放成功booleean isHeldExclusively()
:判断是否是独占式同步状态
查询同步队列中的等待线程情况
Collection<Thread> getQueuedThreads()
:获取等待在同步队列上的线程集合
我们可以通过上面的模板方法自定义同步组件,下面是一个独占锁的实现Mutex
,在同一时刻只允许一个线程占有锁。
1 | public class Mutex implements Lock { |
通过定义静态内部类Sync
继承同步队列器实现自定义同步组件,将锁的操作代理给Mutex
完成,所有锁的操作都是通过Mutex
去调用,上面代码执行后效果与Lock
相同,在同一时刻只允许一个线程获取锁,但是Mutex
类有一个弊端,无法实现可重入,线程获取到锁之后如果再次调用tryLock()
等方法,就会阻塞自己,我阻塞我自己。
AQS使用总结
- AQS用于自定义同步组件,可以在开发中按照自己的意愿开发出同步组件
可重入锁(ReetrantLock)
可重入锁支持同一个线程对已经获取到锁的资源重复加锁,具体有公平锁和非公平锁两种实现。通过构造函数指定锁的公平性。
公平锁和非公平锁的区别在于线程获取锁的优先级,非公平锁可能会导致线程饥饿,公平锁则按线程请求顺序获取锁,先请求先获取。
1 | Lock fairLock = new ReentrantLock(true); |
两种可重入锁
非公平锁
1 | /** |
各种锁的入门使用大全/20200427172022.png)
可以看到线程之间不是按顺序获取锁的
公平锁
1 | package Lock; |
各种锁的入门使用大全/20200427172226.png)
可以看到线程请求顺序获取锁,其原理是在同步组件内部维护着一个同步请求队列,在下一篇会详细说明。
两种可重入锁的优缺点
非公平锁的性能普遍高于公平锁的性能,公平锁保证了锁的获取按照请求顺序FIFO原则,而代价是大量的线程切换,将挂起的线程状态转换成运行态需要恢复现场,延迟很大;而非公平锁可以更好地利用CPU的时间片,减少CPU空闲时间,但容易导致线程饥饿,因为一个线程在释放锁后有极大的概率再次获取锁。
使用场景
公平锁可以对线程进行更强的可控性,而非公平锁则可以提高性能,假如需要对用户的请求顺序进行控制,那么公平锁是一个不错的选择,如果追求任务执行的速度,那么非公平锁将会是一个更佳的选择。
读写锁(ReadWriteLock)
读锁是共享锁(所有线程都可以获取锁),写锁为排他锁(同一时刻只有一个线程可以获取到锁)
应用场景
对集合的访问操作多于更新(插入、更新、删除)操作时可以使用读写锁,典型的Java中的CopyOnWriteArrayList
就是采用读写锁实现的
ReentrantReadWriteLock
ReentrantReadWriteLock特性
- 公平性选择
支持公平性/非公平(默认)的获取方式,吞吐量是非公平高于公平
- 重进入
读线程获取读锁后可以再次获取读锁,写线程获取写锁后可以再次获取写锁,也可以再次获取读锁
- 锁降级
遵循获取写锁、获取读锁再释放写锁的顺序,写锁能降级为读锁
ReentrantReadWriteLock使用方式
readLock()
:获取读锁writeLock()
:获取写锁int getReadLockCount()
:返回读锁被获取的次数int getReadHoldCount()
:返回当前线程获取读锁的次数boolean isWriteLocked()
:判断写锁是否被获取int getWriteHoldCount()
:返回当前线程获取写锁的次数
ReentrantReadWriteLock示例
利用一个HashMap
与Cache作组合实现线程安全的缓存
1 | import java.util.HashMap; |
- 在调用
get()
方法时会申请获取读锁,而读锁是共享锁,所有线程均可以获取到读锁,并发访问下不会阻塞 - 写操作
put(String key, Object value)
和clear()
方法,在被调用时必须提前获取写锁,获取写锁后,其它读锁和写锁请求操作都会被阻塞,只有在写锁释放后,其它读写操作才能继续操作。
读写锁总结
读写锁通过共享读锁提升并发性,使用排他写锁保证每一次写操作都对所有的读写操作可见,这就是该锁高效之处,在日常开发中如果访问操作较多,写操作较少的情况下,读写锁是一种非常高效的工具。
Condition接口(依赖Lock进行使用)
Condition与Lock接口配合实现等待/通知机制,必须由Lock实例调用
newCondition()
返回Condition实例。Condition是一个等待队列,该队列中存储了请求操作未成功的线程,这些线程已经全部成功获取过Lock锁,由于没有完成操作而加入Condition等待队列,进而放弃Lock锁。
Condition使用方式
await()
:释放锁并进入等待状态,其它线程调用signal()
或signalAll()
方法,且当前线程被选中唤醒,则返回awaitUninterruptibly()
:不支持中断的等待状态long awaitNanos(long)
:等待nanos
纳秒,超时、中断或被唤醒则返回,返回值为剩余时间boolean awaitUntil(Date)
:等待直到到达某一个时间点,如果被中断、到达终点、被唤醒则返回,返回值代表是否在终点前被通知signal()
:唤醒在该Condition对象上等待的某个线程signalAll()
:唤醒所有在Condition上等待的线程
Condition使用示例
1 | /** |
- BoundQueue是一个顺序循环队列
- 使用
notEmpty
和notFull
两个等待队列存放等待操作的线程,如果被调用该对象的signal()
/signalAll()
方法会唤醒两个队列的线程重新尝试获取ReentrantLock
锁进行相应操作。
总结
Condition是依赖Lock接口实现等待/通知机制的一个工具,通过Lock
进行对象的获取,在使用等待/通知机制时可以考虑使用Lock
+ Condition
进行使用。
LockSupport工具
LockSupport提供最基本的线程阻塞和唤醒功能
LockSupport使用方式
void park()
:阻塞当前线程,只有调用unpark()
方法或者被中断才能够返回void parkNanos(long nanos)
:阻塞当前线程,最长不超过nanos
纳秒void unpark(Thread thread)
:唤醒处于阻塞状态的线程thread
void parkUntil(long deadline)
:阻塞当前线程,直到deadline
时间(从1970年到deadline的毫秒数)
Java6新增的3个方法
void park(Object blocker)
:阻塞当前线程的某个对象blocker
,该对象主要用于问题排查和系统监控void parkNanos(Object blocker, long nanos)
:阻塞blocker
对象,最长不超过nanos
纳秒void parkUntil(Object blocker, long deadline)
:阻塞blocker
对象,直到deadline
时间(从1970年到deadline的毫秒数)
使用场景
- 用于阻塞和唤醒某个线程,作为构建同步组件的基础工具
总结
这一篇文章对所有关于锁的组件如何使用以及应用场景进行详细的描述和总结,相信会对这些工具会有一个初步的了解,之后会对各个重要的工具的底层实现进行剖析!感谢你的阅读!
巨人的肩膀:
《Java并发编程的艺术》