java.util.concurrent.Locks使用指南

1.概述

简单来说,相对于synchronized ,锁是一种更灵活和精巧的线程同步机制。

Lock接口从Java 1.5后出现,在java.util.concurrent.lock包中定义了Lock接口,并提供了锁的一些扩展操作。

本文,我们将探究一下Lock接口的不同实现和各自的应用。

2.Lock 和 Synchronized 的不同

synchronized和Lock API的使用有一些不同之处:

  • synchronized block 只能在一个方法内– Lock API的 lock() 和 unlock() 操作可以在不同的方法中
  • synchronized 不支持公平锁,锁释放后处于等待的线程都有可能获得锁,也就是不能指定优先级。 而我们却可以通过Lock API指定参数来实现公平锁,确保等待时间最长的线程最先获得锁
  • 如果线程无法访问synchronized块,就会发生阻塞。Lock API提供了 **tryLock()
    方法,只有在锁可用而且没有被其他线程持有时去获得锁** ,这减小了线程等待锁的阻塞时间

  • 处于“waiting”状态的线程获得synchronized块时,不能被中断。而Lock API提供了lockInterruptibly() 方法,可以中断正在等待锁的线程

3.Lock API

首先看一下Lock 接口中的方法:

  • void lock() – 如果锁可用就获得锁,如果锁不可用就阻塞直到锁释放

  • void lockInterruptibly() – 和 lock()方法相似, 但阻塞的线程可中断,抛出 java.lang.InterruptedException异常

  • boolean tryLock() –lock() 方法的非阻塞版本;尝试获取锁,如果成功返回true

  • boolean tryLock(long timeout, TimeUnit timeUnit) – 和tryLock()方法相似,只是 在放弃尝试获取锁之前等待指定的时间。

  • void unlock() – 释放锁

为了防止死锁发生,加锁的实例必须释放锁,建议在try/catch 和 finally 块中使用锁:

Lock lock = ...; 
lock.lock();
try {
    // access to the shared resource
} finally {
    lock.unlock();
}

除了Lock 接口,还有一个ReadWriteLock 接口,ReadWriteLock 接口维护了一对锁。一个只用于读操作,一个用于写操作。只有没有写入操作,读锁可以同时被多个线程持有。
ReadWriteLock 声明了获取读或写的锁:

  • Lock readLock() – 返回读线程的锁
  • Lock writeLock() – 返回写线程的锁

4. Lock 接口的实现

4.1 ReentrantLock

ReentrantLock 类实现了 Lock 接口,不仅提供了跟synchronized 方法和语句使用的隐式monitor锁相同的并发和内存语义 ,而且扩展了其功能。
首先我们看一下 ReenrtantLock 是怎么用于同步的:

public class SharedObject {
    //...
    ReentrantLock lock = new ReentrantLock();
    int counter = 0;

    public void perform() {
        lock.lock();
        try {
            // Critical section here
            count++;
        } finally {
            lock.unlock();
        }
    }
    //...
}

需要确保lock()和 unlock()方法在 try-finally块中,避免死锁发生。
再看看 tryLock() 是怎么工作的:

public void performTryLock(){
    //...
    boolean isLockAcquired = lock.tryLock(1, TimeUnit.SECONDS);

    if(isLockAcquired) {
        try {
            //Critical section here
        } finally {
            lock.unlock();
        }
    }
    //...
}

这里,线程调用tryLock()方法,等待一秒,如果锁不可用就放弃等待。

4.2ReentrantReadWriteLock

ReentrantReadWriteLock 类实现了ReadWriteLock 接口.

以下是线程获取 ReadLock 和 WriteLock 的一些规则:

  • Read Lock – 没有线程获得写锁且没有获取写锁的请求,多个线程可以获得读锁
  • Write Lock – 如果没有线程读或者写,只有一个线程可以获取写锁

我们看看如何使用 ReadWriteLock:

public class SynchronizedHashMapWithReadWriteLock {

    Map  syncHashMap = new HashMap<>();
    ReadWriteLock lock = new ReentrantReadWriteLock();
    //...
    Lock writeLock = lock.writeLock();

    public void put(String key, String value) {
        try {
            writeLock.lock();
            syncHashMap.put(key, value);
        } finally {
            writeLock.unlock();
        }
    }
    ...
    public String remove(String key){
        try {
            writeLock.lock();
            return syncHashMap.remove(key);
        } finally {
            writeLock.unlock();
        }
    }
    //...
}

对于这两种写入方法,临界区需要加写锁,只有一个线程可以访问:

Lock readLock = lock.readLock();
//...
public String get(String key){
    try {
        readLock.lock();
        return syncHashMap.get(key);
    } finally {
        readLock.unlock();
    }
}

public boolean containsKey(String key) {
    try {
        readLock.lock();
        return syncHashMap.containsKey(key);
    } finally {
        readLock.unlock();
    }
}

对于这两种读入方法,临界区需要加读锁,只有没有写的操作,多个线程可以访问该临界区:

4.3 StampedLock类

StampedLock 类在 Java 8中引入,同样支持读锁和写锁,不同的是获取锁的方法返回一个用于释放锁或检查锁是否有效的标记:

public class StampedLockDemo {
    Map map = new HashMap<>();
    private StampedLock lock = new StampedLock();

    public void put(String key, String value){
        long stamp = lock.writeLock();
        try {
            map.put(key, value);
        } finally {
            lock.unlockWrite(stamp);
        }
    }

    public String get(String key) throws InterruptedException {
        long stamp = lock.readLock();
        try {
            return map.get(key);
        } finally {
            lock.unlockRead(stamp);
        }
    }
}

StampedLock 的另一个特点是采用了乐观锁策略,大部分的时间里读操作不需要等待写操作的完成,因此不需要一个完善的读锁,相反可以升级到读锁

public String readWithOptimisticLock(String key) {
    long stamp = lock.tryOptimisticRead();
    String value = map.get(key);

    if(!lock.validate(stamp)) {
        stamp = lock.readLock();
        try {
            return map.get(key);
        } finally {
            lock.unlock(stamp);               
        }
    }
    return value;
}

5. Conditions类

Condition 类提供了在临界区线程可以等待某些条件发生时再去执行。
这种情况发生在当线程获得了临界区的访问但没有必要的条件去执行某些操作。比如,一个读线程获得了共享式队列的锁,但是没有任何数据用于消费。

一般来说,Java提供了wait(), notify() 和 notifyAll() 用于线程间通信,Conditions 类的有着类似的通信机制,除此之外,可以指定多个条件:

public class ReentrantLockWithCondition {

    Stack stack = new Stack<>();
    int CAPACITY = 5;

    ReentrantLock lock = new ReentrantLock();
    Condition stackEmptyCondition = lock.newCondition();
    Condition stackFullCondition = lock.newCondition();

    public void pushToStack(String item){
        try {
            lock.lock();
            while(stack.size() == CAPACITY){
                stackFullCondition.await();
            }
            stack.push(item);
            stackEmptyCondition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public String popFromStack() {
        try {
            lock.lock();
            while(stack.size() == 0){
                stackEmptyCondition.await();
            }
            return stack.pop();
        } finally {
            stackFullCondition.signalAll();
            lock.unlock();
        }
    }
}

6.总结

本文中,我们讨论了Lock 接口的几种不同实现和Java 8才引入的StampedLock 类,同时也探讨了在多个条件下怎么使用Condition 类。


更多内容欢迎关注公众号“大象小蚁”
java.util.concurrent.Locks使用指南_第1张图片

你可能感兴趣的:(个人翻译)