锁(用于控制对共享资源的访问)
Lock接口!!!例如ReentrantLock;
为什么需要使用Lock或者synchronized不够用?
- synchronized效率低,不能设置超时等待,不能中断正常试图获取锁的线程;
- 不够灵活,加锁和释放锁的时机单一,需要执行完任务或者出现异常;
- 无法知道是否成功获取到锁;
方法
lock()
- 获取锁,如果锁被其他线程占用,进行等待;
- Lock不会像synchronized一样在异常时自动释放;
- 使用时使用try—finally,在finally中释放锁;
tryLock()/tryLock(long time,TimeUnit unit)
- 尝试获取锁,返回boolean;
- 可以根据是否获得到锁决定后续的行为;
- 立即返回,就算拿不到锁也不会等待;
- tryLock(long time,TimeUnit unit),超时放弃!
lockInterruptibly()
相当于tryLock(long time,TimeUnit unit)的时间设置为无限,进行等待锁,可以中断,十分灵活;
unlock()
习惯解锁,放在finally中;
可见性保证
happens-before原则;
Lock的加锁解锁和synchronized有同样的内存语义,下一个线程加锁可以看到前一个线程解锁前发生的所有操作;
锁的分类
1. 乐观(非互斥同步)锁和悲观(互斥同步)锁
互斥同步锁的劣势:
- 阻塞和唤醒带来的性能劣势;
- 永久阻塞;
- 优先级反转(例如优先级低的线程拿到了锁并且释放慢,但是优先级高的线程就得等待优先级低的线程执行锁释放);
二者概念
乐观锁(CAS实现):认为自己操作的对象不会有人干扰,不锁同步资源,更新的时候去检查数据是否被其他人修改过,如果没有被修改就正常执行,如果被修改过就不能继续执行了,会选择放弃、报错等策略;例如原子类、并发容器等;
悲观锁:认为自己操作的对象总是会有人干扰,锁住同步资源不让其他人访问就不会出错;例如synchronized和Lock接口;
二者开销(不确定)
总的来说:悲观锁>乐观锁;
悲观锁的开销比较固定,开始也是高于乐观锁的,并且一劳永逸;
虽然乐观锁一开始开销小,但是如果自旋时间长或者不停重试,资源开销也会增加;
适用场景
悲观锁:适合并发写入多的情况,临界区持有锁时间较长,避免大量无用自旋的消耗,典型场景:临界区有IO操作;临界区代码复杂;临界区竞争激烈;
乐观锁:适合并发写入少,大部分是读取的情况,性能提高大;
2. 可重入锁和不可重入锁
可重入锁(递归锁):同一个线程可以多次获取同一把锁;例如synchronized,ReentrantLock;
可重入锁好处:
- 避免死锁(如果两个方法被同一个锁锁住,如果线程A执行第一个方法,再去执行第二个方法的时候,如果没有可重入性质,那么就会死锁);
- 提升封装性(不用一次次的加解锁);
ReentrantLock方法
getHoldCount:返回当前的锁已经被拿到几次;
isHeldByCurrentThread:可以看出锁是否被当前线程所持有;
getQueueLength:可以返回正在等待这把锁的队列有多长;
源码分析
3. 公平锁和非公平锁
公平锁(ReentrantLock构造器传入true):按照线程请求锁的顺序来分配锁;
非公平锁(ReentrantLock默认非公平锁):不完全按照请求顺序来分配锁,在一定情况下可以插队;
非公平锁好处
避免唤醒线程带来的空档期,可以提高效率;
==tryLock()==可以不遵守公平锁策略!!!
二者对比
源码分析
4. 共享锁和排它锁(典型ReentrantReadWriteLock)
排它锁:写锁,独享锁、独占锁;既能读又能写,自己获取后其他线程无法获取;例如synchronized;
共享锁:读锁;获得共享锁后,只能查看数据,无法修改和删除数据,并且其他线程也可以获取到这个共享锁来查看数据;
读写锁的作用
- 读是安全的,多个线程读数据是没有必要加锁的,如果使用同步锁,读数据也是需要获取锁,就造成了没有意义的开销;
- 更加灵活;读的时候用读锁,写的时候用写锁;
读写锁规则
读写锁可以理解成一把锁,有两种情况:要么是多个线程读,要么是一个线程写,并且二者不同时出现;
ReentrantReadWriteLock实现
- 不允许读锁插队(现在运行的是读锁,队列中有读锁和写锁,由于可以同时读,读锁能否插队?);
- 允许降级(写—>读),不允许升级(读—>写);
插队策略
- 公平锁:读写锁都不允许插队;
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(true);
- 非公平锁:
- 写锁可以随时插队(如果线程正在读,写锁是无法插队的,只能进入等待队列);
- 为了防止饥饿,读锁不准插队;读锁只能在等待队列头结点不是想获取写锁的时候可以插队(等待队列第一个是写锁就不能插队,是读锁就可以插队);
源码分析
公平情况下的读写锁
/**
* Fair version of Sync
*/
//公平锁情况下的读写锁
static final class FairSync extends Sync {
private static final long serialVersionUID = -2274990926593161451L;
//写锁是否要阻塞(阻塞就是排队)
final boolean writerShouldBlock() {
//查看队列中是否有线程等待,有就返回true,去排队
return hasQueuedPredecessors();
}
//读锁是否要阻塞(阻塞就是排队)
final boolean readerShouldBlock() {
//查看队列中是否有线程等待,有就返回true,去排队
return hasQueuedPredecessors();
}
}
非公平情况下的读写锁
/**
* Nonfair version of Sync
*/
//非公平情况下的读写锁
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
//写锁是否排队
final boolean writerShouldBlock() {
//写锁不需要排队
return false; // writers can always barge
}
//读锁是否排队
final boolean readerShouldBlock() {
//去查看队列中第一个是不是排它锁(写锁)
//如果是写锁,返回true,排队
//如果是读锁,返回false,插队
return apparentlyFirstQueuedIsExclusive();
}
}
读写锁的升降级策略
支持锁降级(写—>读),不支持升级(读—>写);
Tips:ReentrantReadWriteLock不支持锁升级的原因是避免死锁(例如AB两个线程都是读锁想升级写锁,但是只能有一个写锁进行写入,A要升级就要求B释放读锁,B同理,陷入死锁);
适用场景
ReentrantLock适合一般场合;ReentrantReadWriteLock适合读多写少场合,可以进一步提高并发效率;
5. 自旋锁和阻塞锁
假如等待的锁很快就会被释放(同步代码块中代码简单),就划不来每次都去让CPU切换线程的状态,切换状态的时间比同步代码块执行时间还长;
概念
自旋锁:如果机器有多个处理器,能够让两个或以上的线程并行执行,就可以让后面请求的线程不放弃CPU进行自旋,一直去检测锁是否释放;如果前面的线程释放锁,就可以不必阻塞而直接获得锁,从而减少线程切换带来的开销,这就是自旋锁;
阻塞锁:如果没拿到锁的情况下,直接把线程阻塞,直到被唤醒;
自旋锁缺点
如果锁占用时间过长,自旋造成的开销较大(随着时间增长,自旋锁开销也线性增长),浪费处理器资源;
自旋锁实现原理(底层是CAS)
JDK1.5及以上的JUC下的atmoic基本都是基于自旋锁实现的;
自旋锁适用场景
- 适用于多核服务器,并发度不是特别高;
- 适用于临界区比较简单的情况下;
6. 可中断锁与不可中断锁
- synchronized是不可中断锁;
- Lock是可中断锁,调用tryLock(time)和lockInterruptibly都能响应中断(调用线程的interrupt方法中断);
概念
可中断锁:线程A执行锁中的代码,当线程B在等待线程A释放锁的时候,等待时间过长,线程B不想等待了,于是就可以进行中断,线程B就可以执行其他事情了;
JDK6锁优化
- 自旋锁和自适应(尝试自旋,自旋xx次,不成功就转为阻塞锁;如果这次自旋没得到,下次直接进入阻塞状态);
- 锁消除(代码块不存在并发,无需加锁)
- 锁粗化(如果对一个对象反复加锁解锁,扩大范围合为一个synchronized)
- 偏向锁
- 轻量级锁
- 重量级锁
写代码时如何优化锁和性能提高
- 缩小同步代码块;
- 尽量不要锁住方法,可以使用代码块;
- 减少请求锁的次数;
- 避免人为制造“热点”;
- 锁中尽量不要包含锁;
- 选用合适的锁类型或合适的工具类(例如多读少写用读写锁,并发度不高用原子类);