锁重入
具体的一个场景就是在一个同步域内调用本类的其他由synchronized修饰的方法或代码块时,是永远可以得到锁的
synchronized就拥有锁重入的特性.
能够避免死锁:线程获得某对象的锁后,未释放状态下再次获取该对象的锁时,如果不支持重入,就会造成死锁.
Lock
ReentrantLock
static abstract class Sync extends AbstractQueuedSynchronizer
final static class NonfairSync extends Sync
final static class FairSync extends Sync
排他锁,重入锁 支持公平锁和非公平锁,提供了构造函数允许设置是否为公平锁,默认是非公平锁的
使用
public static void main(String[] args) {
Lock lock = new ReentrantLock();
// Condition condition = lock.newCondition();// 创建Condition,可以存在多个
new Thread(() -> doSome(lock), "thread1").start();
new Thread(() -> doSome(lock), "thread2").start();
}
private static void doSome(Lock lock) {
try {lock.lock();}
......;
finally{lock.unlock();}
}
创建ReentrantLock对象时可以指定锁是否为公平锁,构造方法如下:
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
原理
内部类Sync
ReadWriteLock
ReentrantReadWriteLock
ReentrantLock锁是完全排他锁,同一时间只有一个线程能执行lock.lock()和lock.unlock()之间的代码.
而ReentrantReadWriteLock则通过创建2个实现了Lock接口的内部类ReadLock,WriteLock来维护一对锁,实现性能的提升.
- 读锁:共享,重入
- 写锁:排他,重入
由于写锁是排他的,只要涉及到写操作都是互斥的,一个线程获取到写锁后不释放写锁也能继续获取到读锁,即降级为读锁.
获取锁:
new ReentrantReadWriteLock().readLock();
new ReentrantReadWriteLock().writeLock();
锁状态的记录
ReentrantLock中是使用AQS中的state属性记录锁的状态,ReentrantReadWriteLock有2把锁,其状态怎样记录?
在同一个整形变量上,表示2把锁的状态,就需要将这个变量按位分割使用,高16位记录读锁,低16位记录写锁.
锁优化
锁竞争及上下文切换会导致程序性能的下降,因此对锁的优化也是有必要的.
减少锁持有的时间:和降低锁粒度很相似,不要将耗时的操作放入同步域
减小锁粒度:Java7中ConcurrentHashMap的实现就利用了这种优化方式
读写锁替换独占锁:ReentrantReadWriteLock区别ReentrantLock的地方就体现出这种方式,只有涉及到写操作才会阻塞
锁分离:读写锁的基础上更进一步,LinkedBlockingQueue(基于链表的阻塞队列,读写分离)就使用了这种方式,存在takeLock和putLock两种锁
锁粗化:多个锁合并为同一个锁,与减小锁粒度相反,增大了同步域以避免频繁切换上下文,所以有特殊的适用场景
锁消除:编译器发现不可能被共享的对象,可以消除这些对象的锁操作,如某些类的方法时加了锁,但却用在不会有线程安全问题的情形下
JVM的优化:偏向锁, 轻量级锁, 重量级锁等
JVM的优化
synchronized的锁是互斥锁, 对性能有很大影响, 因此从Java6开始引入了偏向锁和轻量级锁, 以及锁的存储结构和锁升级机制.
synchronized锁是存在Java对象头中, 数组类型用3个字宽(Word)存储对象头, 其他类型用2个字宽(32位VM下1字宽为4字节).
对象头的Mark Word(32位):
其中, 锁状态共有4种, 级别从低到高依次是: 无锁状态, 偏向锁状态, 轻量级锁状态和重量级锁状态, 锁可以升级且不能逆向
自旋锁: 避免挂起线程的一种尝试.如果线程一直无法获得锁, 并且能获取的时间点也未知, 就会让当前线程做几个空循环
偏向锁: 偏向锁就是一旦线程第一次获得了监视对象, 之后让监视对象“偏向”这个线程, 之后的多次调用则可以避免CAS操作
轻量级锁: 偏向锁运行在一个线程进入同步块的情况下, 当第二个线程加入锁竞争的时候, 偏向锁就会升级为轻量级锁
重量级锁: JVM中又叫对象监视器(Monitor), 它至少包含一个竞争锁的队列和一个信号阻塞队列(wait队列), 前者负责做互斥, 后一个用于做线程同步.
死锁预防
以确定的顺序获得锁:在设计时即考虑到不同线程间锁获取的顺序. 超时放弃:使用synchronized获取锁时,只要没有获取到锁就会一直等待,但Lock接口实现了tryLock(),可以指定超时时间,
Q&A
synchronized和ReentrantLock
synchronized和java.util.concurrent.locks.Lock的异同
还包括了中断锁等待和定时锁等待
在并发量小的时候, 用synchronized是比较好的选择, 并发量大的时候用Lock. Lock有比synchronized更精确的线程语义和更好的性能. synchronized是自动释放锁, Lock是主动释放锁, 并且必须在finally语句中释放 Lock可中断, 可以设定所等待的时间, 可尝试获取锁(tryLock(long timeout, TimeUnit unit), 等待可中断), 可绑定多个Condition 有些操作不会发生冲突现象, 需要用Lock解决, 比如同时读文件 synchronized是非公平锁, ReentrantLock默认情况下也是非公平锁, 但可以通过带布尔值的构造函数要求使用公平锁. 都是可重入的.
参考:
Java锁——synchronized和锁(ReentrantLock) 区别
Java锁——AbstractQueuedSynchronizer加锁和解锁分析
Java并发——AQS、AQS到底什么是AQS?这玩意干啥的?
lock的原理是什么?
通过AQS(AbstractQueuedSynchronizer)来维护一个int FIFO等待队列管理获取锁的所有线程. 在“公平锁”的机制下, 线程依次排队获取锁; 而“非公平锁”在锁是可获取状态时, 不管自己是不是在队列的开头都会获取锁.