Lock接口!!!例如ReentrantLock;
相当于tryLock(long time,TimeUnit unit)的时间设置为无限,进行等待锁,可以中断,十分灵活;
习惯解锁,放在finally中;
happens-before原则;
Lock的加锁解锁和synchronized有同样的内存语义,下一个线程加锁可以看到前一个线程解锁前发生的所有操作;
互斥同步锁的劣势:
- 阻塞和唤醒带来的性能劣势;
- 永久阻塞;
- 优先级反转(例如优先级低的线程拿到了锁并且释放慢,但是优先级高的线程就得等待优先级低的线程执行锁释放);
乐观锁(CAS实现):认为自己操作的对象不会有人干扰,不锁同步资源,更新的时候去检查数据是否被其他人修改过,如果没有被修改就正常执行,如果被修改过就不能继续执行了,会选择放弃、报错等策略;例如原子类、并发容器等;
悲观锁:认为自己操作的对象总是会有人干扰,锁住同步资源不让其他人访问就不会出错;例如synchronized和Lock接口;
总的来说:悲观锁>乐观锁;
悲观锁的开销比较固定,开始也是高于乐观锁的,并且一劳永逸;
虽然乐观锁一开始开销小,但是如果自旋时间长或者不停重试,资源开销也会增加;
悲观锁:适合并发写入多的情况,临界区持有锁时间较长,避免大量无用自旋的消耗,典型场景:临界区有IO操作;临界区代码复杂;临界区竞争激烈;
乐观锁:适合并发写入少,大部分是读取的情况,性能提高大;
可重入锁(递归锁):同一个线程可以多次获取同一把锁;例如synchronized,ReentrantLock;
getHoldCount:返回当前的锁已经被拿到几次;
isHeldByCurrentThread:可以看出锁是否被当前线程所持有;
getQueueLength:可以返回正在等待这把锁的队列有多长;
公平锁(ReentrantLock构造器传入true):按照线程请求锁的顺序来分配锁;
非公平锁(ReentrantLock默认非公平锁):不完全按照请求顺序来分配锁,在一定情况下可以插队;
避免唤醒线程带来的空档期,可以提高效率;
排它锁:写锁,独享锁、独占锁;既能读又能写,自己获取后其他线程无法获取;例如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适合读多写少场合,可以进一步提高并发效率;
假如等待的锁很快就会被释放(同步代码块中代码简单),就划不来每次都去让CPU切换线程的状态,切换状态的时间比同步代码块执行时间还长;
自旋锁:如果机器有多个处理器,能够让两个或以上的线程并行执行,就可以让后面请求的线程不放弃CPU进行自旋,一直去检测锁是否释放;如果前面的线程释放锁,就可以不必阻塞而直接获得锁,从而减少线程切换带来的开销,这就是自旋锁;
阻塞锁:如果没拿到锁的情况下,直接把线程阻塞,直到被唤醒;
如果锁占用时间过长,自旋造成的开销较大(随着时间增长,自旋锁开销也线性增长),浪费处理器资源;
JDK1.5及以上的JUC下的atmoic基本都是基于自旋锁实现的;
- 适用于多核服务器,并发度不是特别高;
- 适用于临界区比较简单的情况下;
可中断锁:线程A执行锁中的代码,当线程B在等待线程A释放锁的时候,等待时间过长,线程B不想等待了,于是就可以进行中断,线程B就可以执行其他事情了;
- 缩小同步代码块;
- 尽量不要锁住方法,可以使用代码块;
- 减少请求锁的次数;
- 避免人为制造“热点”;
- 锁中尽量不要包含锁;
- 选用合适的锁类型或合适的工具类(例如多读少写用读写锁,并发度不高用原子类);