Locks使用指南

1. 概述

简单地说,锁是一种比标准同步块更灵活、更复杂的线程同步机制。

Lock接口从 Java 1.5 开始就已经存在了。它是在java.util.concurrent.lock包中定义的,它提供了广泛的锁定操作。

在本教程中,我们将探讨Lock接口的不同实现及其应用程序。

2. 锁定块和同步块的区别

使用同步和使用锁定API 之间存在一些区别:

  • 同步块完全包含在方法中。我们可以在单独的方法中拥有LockAPI lock() 和unlock() 操作。
  • 一个 inchronoized块不支持公平性。任何线程都可以在释放后获取锁,并且不能指定首选项。我们可以通过指定公平性属性在锁定API 中实现公平性。它确保最长的等待线程可以访问锁。
  • 如果线程无法访问同步,则会被阻止。Lock API 提供了tryLock() 方法。仅当线程可用且未由任何其他线程持有时,线程才会获取锁。这减少了线程等待锁定的阻塞时间。
  • 处于“等待”状态以获取对同步块的访问权限的线程不能中断。LockAPI 提供了一个方法lockInterruptibly(),可用于在线程等待锁定时中断线程。

3.锁定接口

让我们看一下Lock接口中的方法:

  • void lock()– 获取锁(如果可用)。如果锁不可用,则线程将被阻塞,直到释放锁。
  • void lockInterruptibly()– 这类似于lock(),但它允许被阻塞的线程被中断并通过抛出的java.lang.InterruptedException 恢复执行。
  • boolean tryLock()– 这是lock() 方法的非阻塞版本。它尝试立即获取锁,如果锁定成功,则返回 true。
  • boolean tryLock(long timeout, TimeUnit timeUnit) – 这类似于tryLock(),只是它在放弃尝试获取Lock之前等待给定的超时。
  • voidunlock() 解锁Lock实例。

锁定的实例应始终解锁以避免死锁情况。

使用锁的推荐代码块应包含try/catchfinally块:

代码语言:javascript代码运行次数:0运行复制
Lock lock = ...; 
lock.lock();
try {
    // access to the shared resource
} finally {
    lock.unlock();
}Copy

除了 Lock 接口之外,我们还有一个ReadWriteLock接口,它维护一对锁,一个用于只读操作,一个用于写入操作。只要没有写入,读锁定就可以由多个线程同时持有。

ReadWriteLock声明了获取读锁或写锁的方法:

  • Lock readLock() 返回用于读取的锁。
  • Lock writeLock() 返回用于写入的锁。

4. 锁定实现

4.1.重入锁

ReentrantLock类实现Lock接口。它提供与使用同步方法和语句访问的隐式监视器锁相同的并发性和内存语义,并具有扩展功能。

让我们看看如何使用ReentrantLock进行同步:

代码语言:javascript代码运行次数:0运行复制
public class SharedObject {
    //...
    ReentrantLock lock = new ReentrantLock();
    int counter = 0;

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

我们需要确保将lock() 和unlock() 调用包装在try-finally块中,以避免死锁情况。

让我们看看tryLock() 是如何工作的:

代码语言:javascript代码运行次数:0运行复制
public void performTryLock(){
    //...
    boolean isLockAcquired = lock.tryLock(1, TimeUnit.SECONDS);
    
    if(isLockAcquired) {
        try {
            //Critical section here
        } finally {
            lock.unlock();
        }
    }
    //...
}
Copy

在这种情况下,调用tryLock() 的线程将等待一秒钟,如果锁不可用,则会放弃等待。

4.2.重入读写锁定

ReentrantReadWriteLock类实现ReadWriteLock接口。

让我们看看通过线程获取ReadLockWriteLock的规则:

  • 读锁定 – 如果没有线程获取写锁定或请求写锁定,则多个线程可以获取读锁定。
  • 写锁定 – 如果没有线程在读取或写入,则只有一个线程可以获取写锁定。

让我们看看如何使用读写锁

代码语言:javascript代码运行次数:0运行复制
public class SynchronizedHashMapWithReadWriteLock {

    Map<String,String> 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();
        }
    }
    //...
}Copy

对于这两种写入方法,我们需要用写锁包围关键部分 — 只有一个线程可以访问它:

代码语言:javascript代码运行次数:0运行复制
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();
    }
}Copy

对于这两种读取方法,我们需要用读锁定包围关键部分。如果没有正在进行的写入操作,则多个线程可以访问此部分。

4.3.StampedLock

StampedLock是在Java 8中引入的。它还支持读锁和写锁。

但是,锁获取方法返回一个标记,用于释放锁定或检查锁定是否仍然有效:

代码语言:javascript代码运行次数:0运行复制
public class StampedLockDemo {
    Map<String,String> 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);
        }
    }
}Copy

StampedLock提供的另一个功能是乐观锁定。大多数情况下,读取操作不需要等待写入操作完成,因此不需要成熟的读锁定。

相反,我们可以升级到读锁定:

代码语言:javascript代码运行次数:0运行复制
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;
}Copy

5. 条件

Condition类使线程能够在执行关键部分时等待某些条件发生。

当线程获取对关键部分的访问权限但没有执行其操作的必要条件时,可能会发生这种情况。例如,读取器线程可以访问仍然没有任何数据可供使用的共享队列的锁。

传统上,Java 提供 wait()、notify() 和notifyAll() 方法来进行线程互通。

条件s 具有类似的机制,但我们也可以指定多个条件:

代码语言:javascript代码运行次数:0运行复制
public class ReentrantLockWithCondition {

    Stack<String> 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();
        }
    }
}Copy

6. 结论

在本文中,我们看到了 Lock 接口和新引入的StampedLock类的不同实现。

我们还探讨了如何利用Condition类来处理多个条件。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。 原始发表:2023-02-14,如有侵权请联系 cloudcommunity@tencent 删除javalock接口同步线程