本系列主要深入解析Java并发框架的内容,这是第三部分的内容,分析java并发中遇到的各种各样的锁的特点以及适用场景。
Lock接口
Lock和Syncronized是两个最常见的锁。Lock接口最常见的实现类是ReentrantLock
。
Synchronized缺点:
- 效率低:锁的释放困难,无法灵活释放锁。
- 无法知道是否成功获取锁。
Lock接口中的方法
调用这些方法时,一定是要创建完成实例的,然后调用锁实例的这方法。
lock()
: 最普通的方法,不会在异常时自动释放锁。因此最佳实践是,在finally中释放锁, 保证发生异常时锁可以被释放。
缺点: 无法被中断,一旦陷入死锁,lock()就会进入永久等待。
tryLock()
: 尝试获取锁,返回boolean。这个方法是可以立刻返回的。tryLock(long time, TimeUnit )
: 等待一定时间,然后判断。lockInterruptibly()
: 等待时间无线,等待锁的过程中,线程可以被中断。unlock()
: 一定要放在finally()
锁的分类
乐观锁与悲观锁
互斥锁的缺点:
阻塞带来的性能缺点。 死锁缺陷。**乐观锁是免疫死锁的。**
乐观锁(CAS):认为发生冲突是小概率的。操作时候不锁住对象,在更新时候进行检查,发生了冲突在回滚修复。典型例子:原子类,并发容器。
适用场合:
悲观锁: 1. 临界区有IO操作;2. 临界区代码复杂; 3. 临界区竞争激烈 乐观锁: 适合并发写入少,大部分是读取的场合。
可重入锁与非可重入锁
可重入:可以一个线程请求多个锁。可以在一定程度上减小死锁。取得了某把锁的线程可以继续获得这个锁。在拿到锁之后继续可以拿到其他的锁。
源码实现:AQS框架实现的。
典型例子:ReentrantLock()
公平与非公平
- 公平锁就是按照线程请求的顺序来分配锁;非公平锁在需要的场合可以插队。
优点:一定程度上,非公平锁是可以提高效率,避免唤醒带来的空档期。
缺点:可能带来饥饿,一些线程永远等不到锁。
公平锁:new ReetrantLock(true)
共享锁与排他锁
一个锁是否只能一个线程持有。典型的共享锁就是ReentantReadWriteLock
,对于读是完全没有干扰的,但是写是封闭的。
1 | ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(); |
- 读写锁的交互方法:默认不许插队,但是可以锁的降级。
公平锁:完全不许插队
非公平锁:写锁可以随时插队,读锁只在等待队头不是写锁时候插队。
降级:从写锁变为读锁。但是不能反过来。在writelock.lock()
情况下可以请求readlock.lock()
。升级会容易造成死锁,使用写锁的前提是不能有读锁,因此可能导致死锁(两个同时升级)。
自旋锁和阻塞锁
线程的状态转换消耗的时间比用户执行代码需要的视角还多。这时对线程执行自旋,完成自旋以后再判断是不是可以获取锁。
缺点:如果前面的线程一直占着锁,其实是浪费cpu资源。
atomic包下的类基本都是自旋锁实现的,AtomicInteger的实现,自旋锁的原理是CAS,再调用unsafe自增是,就是一直while死循环,直到修改成功。
锁优化(JVM帮助实现)
- 自旋锁和自适应:循环了几次之后自己放弃自旋锁的方法。
- 锁消除:对于没有必要使用的锁不使用
- 锁粗化:合并几个锁。