Lock和Condition

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;
}

  

参考资料:
《Java并发编程实战》
《jva核心技术36讲》
 
posted @ 2019-07-09 22:09  morphの  阅读(229)  评论(0编辑  收藏  举报