带你进入java.util.concurrent.Locks

1.概览

简单地说,比起标准的同步块来说,lock是一个更加灵活、更加精密的线程同步机制。Lock是从java1.5开始推出的,它定义在java.util.concurrent.Lock包里,它提供了更广泛的锁操作。在这篇文章中,我们将探讨一些Lock接口的不同实现以及他们的应用。


2. Lock锁和同步块的区别

同步块和Lock API的些许区别:

1.同步块只能包含在一个方法内----而lock()和unlock()操作却可以在跨越多个不同的方法使用。

 2.同步块不支持公平性,任一个线程都能获取已经被释放的锁,不能指定优先权。但我们却可以使用Lock API指定公平属性从而实现公平性。它能确保等待时间最长的线程优先获取锁。

  3.当一个线程不能访问同步块时,它会被阻塞住。而 Lock API提供的有 tryLock()方法,使用该方法,只有在锁不被其他线程持有且可用时,才会真正获取锁。这将极大地降低阻塞时间。

  4.那些获取访问同步块的等待线程不能被中断,Lock API提供了一个 lockInterruptbly()方法,当线程正在等待锁时,该方法可以用于中断该线程。


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()  -解锁该锁实例。

一个锁实例应该永远处于解锁状态,这样才能避免死锁条件。使用lock的推荐方式是:代码块中应该包含try/catch 以及finally 块。

Lock lock = ...;

lock.lock();

try{

    // access to the shared resource

} finally{

    lock.unlock();

}


除了Lock接口之外,我们还有一个读写锁ReadWriteLock接口,此接口包含了一对锁,一个用于只读操作,一个用于写操作。只要没有写操作,读锁可以同时被多个线程持有。

ReadWriteLock 所声明的用于获取读、写锁的方法:

.Lock  readLock()  - 返回用于读操作的锁。

.Lock  writeLock()  - 返回用于写操作的锁。



4.Lock实现


 4.1 ReentrantLock

   ReentrantLock类实现了Lock接口。它除了提供和synchronized代码块一样的并发和内存语义还拥有可扩展的能力。

   我们来看一下,如何使用ReentrantLock做同步:

  public class SharedObject {

    //...

    ReentrantLock lock = new ReentrantLock();

    int counter = 0;

    public void perform() {

        lock.lock();

        try {

            // Critical section here

            count++;

        } finally {

            lock.unlock();

        }

    }

    //...

}

我们需要确保在try-finally块中包装了lock()和unlock()方法,只有这样才能避免死锁。

我们来看一下,tryLock() 是如何工作的:

public void performTryLock(){

    //...

    boolean isLockAcquired = lock.tryLock(1, TimeUnit.SECONDS);

    if(isLockAcquired) {

        try {

            //Critical section here

        } finally {

            lock.unlock();

        }

    }

    //...

}

在这个案例中,调用tryLock()的线程将等待1秒钟,如果锁不可用则放弃等待。


4.2 ReentrantReadWriteLock

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();

        }

    }

    //...

}

对于这两个写方法,我们都需要用write lock来包围临界区,只有一个线程可以访问它。

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();

    }

}

对于上面的俩个读方法,我们需要用读锁read lock 来包围临界区。在没有写操作的情况下,多个线程可以同时访问临界区。


4.3 StampedLock

StampedLock是在java8中引进的。它也同时支持读锁和写锁。然而 获取锁的方法会返回一个标记,这个标记stamp可以用于释放锁或检查该锁是否依然有效。

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提供的另一个特性是:乐观锁。 大多数时候,读操作不需要等待写操作完成。正是基于此,我们并不需要一个完全的读锁(the full fledged read lock is not required)。作为替代,我们可以使用读锁。

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()方法用于线程间通信,Condition也有相似的机制,但是除此之外,我们可以指定多个条件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接口的不同实现以及新引入的StampedLock类。我们也探索一下如何在多条件下使用Condition。

文章中的完整代码都在github上:sourceCode

你可能感兴趣的:(带你进入java.util.concurrent.Locks)