Lock是java.util.concurrent(java并发包)中的接口,用于解决线程安全问题。
既然synchronized可以解决线程同步问题为什么还会有lock?
这是因为使用synchronized申请资源的时候,如果资源被占有,那么线程就进入阻塞状态,而且无法主动释放资源。
而Lock可以
这三种方案可以弥补 synchronized 的问题,体现在 API 上,就是 Lock 接口的三个方法。详情如下:
// 支持中断的 API void lockInterruptibly() throws InterruptedException; // 支持超时的 API boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 支持非阻塞获取锁的 API boolean tryLock();
下面是锁的类图:
可以看到并发包中有可重入锁和读写锁实现了lock接口。
先介绍一下ReentrantLock(可重入锁),指线程可以重复获取同一把锁,在使用 ReentrantLock 的时候,你会发现 ReentrantLock 这个类有两个构造函数,一个是无参构造函数,一个是传入 fair 参数的构造函数。fair 参数代表的是锁的公平策略,如果传入 true 就表示需要构造一个公平锁,反之则表示要构造一个非公平锁。
ReentrantLock fairLock = new ReentrantLock(true);
这里所谓的公平性是指在竞争场景中,当公平性为真时,会倾向于将锁赋予等待时间最久的线程。公平性是减少线程“饥饿”(个别线程长时间等待锁,但始终无法获取)情况发生的一个办法, 当然sychronized无法保证公平性。
简单介绍下使用方法:
ReentrantLock lock = new ReentrantLock(); lock.lock(); try { // Todo } finally { lock.unlock(); }
这样做是为了保证锁的释放,每一个lock()动作,建议都立即对应一个try-catch-fnally。
Condition
条件变量(java.util.concurrent.Condition),如果说ReentrantLock是synchronized的替代选择,Condition则是将wait、notify、notifyAll等操作转化为相应的对象,将复杂而晦涩的同步操作转变为直观可控的对象行为。
先看下阻塞队列的源码理解一下condition的用法:
final ReentrantLock lock; private final Condition notEmpty; private final Condition notFull; public ArrayBlockingQueue(int var1, boolean var2) { this.itrs = null; if (var1 <= 0) { throw new IllegalArgumentException(); } else { this.items = new Object[var1]; this.lock = new ReentrantLock(var2); //通过锁获取条件变量 this.notEmpty = this.lock.newCondition(); this.notFull = this.lock.newCondition(); } } public E take() throws InterruptedException { ReentrantLock var1 = this.lock; var1.lockInterruptibly(); Object var2; try { //如果队列为空则等待 while(this.count == 0) { this.notEmpty.await(); } var2 = this.dequeue(); } finally { var1.unlock(); } return var2; } private void enqueue(E var1) { Object[] var2 = this.items; var2[this.putIndex] = var1; if (++this.putIndex == var2.length) { this.putIndex = 0; } ++this.count; //元素入队时唤醒阻塞在notEmpty条件变量上的线程 this.notEmpty.signal(); }
通过signal/await的组合,完成了条件判断和通知等待线程,这和 wait()、notify()、notifyAll() 是相同的,但是不一样的是后者只有在 synchronized 实现的管程里才能使用。
ReentrantReadWriteLock
可重入读写锁,lock的另外一种实现方式,同样支持公平与非公平,与ReentrantLock这种互斥类型的锁不同的是,读写锁允许多个线程同时读共享变量但是写操作互斥。应用场景就是适合读多写少,比如缓存。这样根据不同场景使用不同的锁,可以提升性能。
缓存代码示例如下:
public class CacheDemo<K, V> { final Map<K, V> m = new HashMap<>(); final ReadWriteLock rwl = new ReentrantReadWriteLock(); // 读锁 final Lock r = rwl.readLock(); // 写锁 final Lock w = rwl.writeLock(); // 读缓存 V get(K key) { r.lock(); try { return m.get(key); } finally { r.unlock(); } } // 写缓存 V put(K key, V v) { w.lock(); try { return m.put(key, v); } finally { w.unlock(); } } }
这里读缓存存在一个问题,有可能缓存不存在那么需要从数据库重新读取,所以修改读缓存如下:
V get(K key){ // 读缓存 r.lock(); try { V v = m.get(key);
//如果缓存不存在需要从数据库读取 if (v == null) {
//此时需要写锁,因为有更新缓存的操作 w.lock(); try { //查询数据库 //更新并返回缓存 } finally{ w.unlock(); } } } finally{ r.unlock(); } }
这样看上去好像是没有问题的,先是获取读锁,然后再升级为写锁,对此还有个专业的名字,叫 “锁的升级”。然而读写锁并不支持这种升级操作,如果这里读锁没释放就获取写锁,会导致写锁一直等待下去,造成线程阻塞。
不过,虽然锁的升级是不允许的,但是锁的降级却是允许的,既支持写锁降级为读锁。
StampedLock
读写锁虽然比ReentrantLock的粒度似乎细一些,但由于较大的开销性能仍然不高。所以,JDK在后期引入了StampedLock,它的性能更优,支持三种模式:写锁、悲观读锁和乐观锁。其语义和 读写锁的语义非常类似,允许多个线程同时获取悲观读锁,但是只允许一个线程获取写锁,写锁和悲观读锁是互斥的。
StampedLock不支持重入,也不支持中断,如果需要支持中断功能,一定使用可中断的悲观读锁 readLockInterruptibly() 和写锁 writeLockInterruptibly()。
StampedLock 的性能之所以比 ReadWriteLock 还要好,其关键是 StampedLock 支持乐观读的方式,而乐观读是无锁的。乐观读的实现原理:假设大多数情况下读操作并不会和写操作冲突,其逻辑是先试着修改,然后通过validate方法确认是否进入了写模式,如果没有进入,就成功避免了开销;如果进入,则尝试获取读锁。 它的写锁和悲观读锁加锁成功之后,都会返回一个 stamp,然后解锁的时候,需要传入这个 stamp。
关于乐观读的伪代码如下列代码所示:
private final StampedLock sl = new StampedLock(); void mutate() { long samp = sl.writeLock(); try { //写数据 write(); } finally { sl.unlockWrite(samp); } } Object access() { long samp = sl.tryOptimisticRead(); //读数据 Data data = read(); //校验samp,检查是否持有写锁 if (!sl.validate(samp)) { //如果持有写锁就升级为悲观读锁 samp = sl.readLock(); try { //重新读数据 data = read(); } finally { sl.unlockRead(samp); } } //如果没有持有写锁就直接返回 return data; }
原文:https://www.cnblogs.com/morph/p/11158165.html