0
点赞
收藏
分享

微信扫一扫

深入解析ReentrantLock与StampedLock的使用技巧

ReentrantLock

1. 概要介绍

1.1 ReentrantLock 背景和定义

在多线程并发编程中,锁是一种基础且关键的同步机制,它帮助我们协调不同线程之间对共享资源的访问,确保数据的一致性和完整性。ReentrantLock,即“可重入锁”,是 java.util.concurrent.locks 包中的一个类,它实现了 Lock 接口并提供了与 synchronized 关键字相似的同步功能。与 synchronized 相比,ReentrantLock 提供了更灵活的锁定操作,并支持更丰富的功能。

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private final ReentrantLock lock = new ReentrantLock();

    public void performLocking() {
        lock.lock();
        try {
            // 保护的临界区代码
        } finally {
            lock.unlock();
        }
    }
}

1.2 ReentrantLock 和 synchronized 的比较

1.2.1 基本差异

synchronized 是 Java 的内置关键字,提供了对对象进行原子性操作的能力;而 ReentrantLock 是一个 Java 类,需要通过显式的锁定(lock())和解锁(unlock())方法来实现同步。不仅如此,ReentrantLock 还提供了尝试非阻塞地获取锁(tryLock())、可中断的锁获取(lockInterruptibly())以及实现公平锁等高级功能。

1.2.2 性能对比

在单一锁竞争较少的场景中,两者性能差别不大。但是在高并发、锁竞争激烈的情况下,ReentrantLock 的性能通常优于 synchronized,因为 ReentrantLock 提供了尝试锁、定时锁等高级功能,这些功能使开发者能够更精细地控制锁的行为,从而在一些复杂的同步场景下有更高的性能。

1.2.3 场景适应性

synchronized 由于其简单性,非常适合那些代码结构简单、同步需求不高的场景。而 ReentrantLock 则适用于更复杂的并发场景,如需要公平性、可中断、条件锁等高级同步特性时。

2. ReentrantLock 的高级功能

2.1 公平锁与非公平锁

ReentrantLock 允许创建公平锁或非公平锁:公平锁意味着在多个线程竞争的情况下,锁的分配将遵循 FIFO 规则;非公平锁则是在竞争时不考虑等待时间,可能会存在“插队”的情况。通常情况下,非公平锁的性能高于公平锁,因为后者要维护一个有序队列。

2.2 条件变量(Conditions)

条件变量可以用于更细粒度的线程协调。ReentrantLock 与条件变量 Condition 结合使用时,可以让线程在某些条件不满足时暂停执行(通过 Condition.await() 方法),直到另外一个线程改变条件并通知 Condition(通过 Condition.signal() 或 Condition.signalAll() 方法)。

2.3 可中断的锁获取

ReentrantLock 提供了 lockInterruptibly() 方法,允许在等待锁的过程中响应中断。这是 synchronized 所不支持的功能,它可以帮助你处理死锁或长时间等待锁的问题,在合适的时候安全地终止线程的执行。

2.4 锁的粗细化与优化

ReentrantLock 让开发者有机会进行更精细的锁管理。通过分离多个锁,我们可以仅在需要保护的资源上加锁,这样可以减少竞争并提高效率。

3. ReentrantLock 的实践应用

3.1 实现一个线程安全的计数器

计数器是并发程序中最简单的共享资源之一。使用 ReentrantLock 可以确保在多线程环境下更新计数器的安全性。

public class Counter {
    private int count = 0;
    private final ReentrantLock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        return count;
    }
}

3.2 构建一个简单的阻塞队列

阻塞队列是多线程编程中的一个常用组件,用于线程间的数据交换和协调。下列代码示例展示了如何利用 ReentrantLock 和条件变量 Condition 来构建一个线程安全的阻塞队列。

import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class BlockingQueue<T> {
    private final LinkedList<T> queue = new LinkedList<>();
    private final int capacity;
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();

    public BlockingQueue(int capacity) {
        this.capacity = capacity;
    }

    public void put(T element) throws InterruptedException {
        lock.lock();
        try {
            while (queue.size() == capacity) {
                notFull.await();
            }
            queue.add(element);
            notEmpty.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public T take() throws InterruptedException {
        lock.lock();
        try {
            while (queue.size() == 0) {
                notEmpty.await();
            }
            T item = queue.removeFirst();
            notFull.signalAll();
            return item;
        } finally {
            lock.unlock();
        }
    }
}

3.3 实现多条件的生产者消费者问题

生产者-消费者问题是一个典型的同步问题。通过使用 ReentrantLock 的条件变量 Condition,我们可以在同一个锁上创建多个条件队列,分别为生产者和消费者提供等待队列,如下所示。

public class ProducerConsumerExample {
    private LinkedList<Integer> buffer = new LinkedList<>();
    private int maxSize = 10;
    private ReentrantLock lock = new ReentrantLock();
    private Condition full = lock.newCondition();
    private Condition empty = lock.newCondition();

    class Producer implements Runnable {
        @Override
        public void run() {
            while (true) {
                lock.lock();
                try {
                    while (buffer.size() == maxSize) {
                        full.await();
                    }
                    buffer.add((int) (Math.random() * 1000));
                    empty.signalAll();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        }
    }

    class Consumer implements Runnable {
        @Override
        public void run() {
            while (true) {
                lock.lock();
                try {
                    while (buffer.isEmpty()) {
                        empty.await();
                    }
                    int value = buffer.poll();
                    System.out.println("Consumed: " + value);
                    full.signalAll();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        }
    }
}

在这个示例中,我们有一个共享的 buffer,通过 lock 保证线程安全。Producer 在 buffer 满时等待,Consumer 在 buffer 空时等待。使用 full.await() 和 empty.await() 来挂起线程,full.signalAll() 和 empty.signalAll() 来唤醒等待的线程。

ReentrantReadWriteLock

1. 概要介绍

1.1 ReentrantReadWriteLock 的作用和设计初衷

ReentrantReadWriteLock是一种读写锁,它允许多个线程同时读取一个资源而不会发生冲突,但是如果有线程想要写入资源,则必须独占访问权。这种锁适用于读操作远多于写操作的场景,因为它可以提高程序的性能和吞吐量。

1.2 读写锁的工作原理

读写锁维护了一对锁,一个读锁和一个写锁。当没有线程持有写锁时,多个线程可以同时获得读锁。但是一旦有线程请求了写锁,其他线程就无法获得读锁或写锁,保证了写入时的独占访问。

2. ReentrantReadWriteLock 的实践应用

2.1 缓存系统中的使用案例

在缓存系统中,数据的读取次数往往远远大于更新次数。使用 ReentrantReadWriteLock 可以提高缓存系统读取数据时的并发性能。

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class CacheWithReadWriteLock {
    private final Map<String, Object> cacheMap = new HashMap<>();
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
    private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();

    public Object get(String key) {
        readLock.lock();
        try {
            return cacheMap.get(key);
        } finally {
            readLock.unlock();
        }
    }

    public Object put(String key, Object value) {
        writeLock.lock();
        try {
            return cacheMap.put(key, value);
        } finally {
            writeLock.unlock();
        }
    }

    // ...其他方法
}

2.2 读多写少场景中的性能优化

在档案系统或配置中心等读多写少的场景中,ReentrantReadWriteLock 通过允许并发读取,可以极大地提高这类系统的性能。

2.3 实现线程安全的数据结构,如Map

下面的示例代码展示了如何使用 ReentrantReadWriteLock 实现线程安全的 Map:

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ThreadSafeMap<K, V> {
    private final Map<K, V> map = new HashMap<>();
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
    private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();

    public V get(K key) {
        readLock.lock();
        try {
            return map.get(key);
        } finally {
            readLock.unlock();
        }
    }

    public void put(K key, V value) {
        writeLock.lock();
        try {
            map.put(key, value);
        } finally {
            writeLock.unlock();
        }
    }

    // ...其他方法
}

在这个例子中,对 Map 的读取操作使用了读锁,更新操作使用了写锁。这样可以保证在更新数据时,所有的读操作都会等待,直到写操作完成。

StampedLock

1. 概要介绍

1.1 StampedLock 的设计与特点

StampedLock 是在 Java 8 中引入的,它提供了一种乐观读锁的实现,这可以在没有写入时增加程序的并发度。与 ReentrantReadWriteLock 不同,StampedLock 的锁定方法会返回一个标记(stamp)以表示锁的状态。

1.2 与 ReadWriteLock 的对比

StampedLock 支持三种模式的锁:写锁、悲观读锁和乐观读。乐观读是 StampedLock 与 ReadWriteLock 最大的不同之处。乐观读允许完全无锁的访问,只在数据完整性上检查有无冲突,提供了一种无锁的读取方式,这通常用于数据结构的维护中。

2. StampedLock 的实践应用

2.1 StampedLock 在几何计算中的应用

在几何计算应用中,读取数据的操作远多于写入操作。下面是一个使用 StampedLock 管理几何形状数据结构的例子:

import java.util.concurrent.locks.StampedLock;

public class Point {
    private double x, y;
    private final StampedLock sl = new StampedLock();

    void move(double deltaX, double deltaY) {
        long stamp = sl.writeLock();
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            sl.unlockWrite(stamp);
        }
    }

    double distanceFromOrigin() {
        long stamp = sl.tryOptimisticRead();
        double currentX = x, currentY = y;
        if (!sl.validate(stamp)) {
            stamp = sl.readLock();
            try {
                currentX = x;
                currentY = y;
            } finally {
                sl.unlockRead(stamp);
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }

    // ...其他方法
}

在这个例子中,move() 方法使用写锁来改变点的位置,而 distanceFromOrigin() 方法首先尝试一个乐观读,然后验证返回的标记,如果标记无效(说明在读取数据的同时有其他线程进行了写操作),则升级到悲观读锁以确保数据的一致性。

2.2 实现乐观读取的数据结构

乐观读取通常用于数据很少修改的情景,以下是利用 StampedLock 实现的一个线程安全并且支持乐观读取的数据结构例子。

import java.util.concurrent.locks.StampedLock;

public class OptimisticReadExample {
    private volatile int value = 0;
    private final StampedLock lock = new StampedLock();

    public void update(int newValue) {
        long stamp = lock.writeLock();
        try {
            value = newValue;
        } finally {
            lock.unlockWrite(stamp);
        }
    }

    public int read() {
        long stamp = lock.tryOptimisticRead();
        int readValue = value;
        // 验证乐观读后,数据是否被其他线程更改
        if (!lock.validate(stamp)) {
            // 乐观读失败,升级为悲观读锁
            stamp = lock.readLock();
            try {
                readValue = value;
            } finally {
                lock.unlockRead(stamp);
            }
        }
        return readValue;
    }
}

在上述代码中,update() 方法通过写锁来保证数据更新的原子性。而 read() 方法首先尝试乐观读取,通过验证 stamp 来确认在读取过程中数据是否被修改,如果被修改,则通过获取悲观读锁来保证数据读取的一致性。

Condition

1. 概要介绍

1.1 Condition 接口的概念与用途

Condition接口提供了一种在特定Lock对象上等待的手段,这比传统的对象监视方法(wait、notify和notifyAll)提供了更丰富的线程控制手段。一个Lock对象可以绑定多个Condition对象,它们可以控制线程的暂停(await())和唤醒(signal()/signalAll())。

1.2 Condition 与 Object 监视器方法的比较

Condition相比于传统的监视器方法,其优势在于支持多个等待集,即可以有多个线程等待条件的队列,这在某些算法和数据结构设计中更为高效和灵活。

2. Condition 的实践应用

2.1 使用Condition实现有界队列

Condition可以用于有界队列的实现,它可以协助处理队列的空和满的状态。下面是一个使用ReentrantLock和Condition实现的有界队列示例:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class BoundedQueue<T> {
    private Object[] items;
    // 添加的下标,删除的下标和当前数量
    private int addIndex, removeIndex, count;
    private ReentrantLock lock = new ReentrantLock();
    private Condition notEmpty = lock.newCondition();
    private Condition notFull = lock.newCondition();

    public BoundedQueue(int size) {
        items = new Object[size];
    }

    public void add(T t) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length) {
                notFull.await();
            }
            items[addIndex] = t;
            if (++addIndex == items.length) {
                addIndex = 0;
            }
            ++count;
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    @SuppressWarnings("unchecked")
    public T remove() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                notEmpty.await();
            }
            Object x = items[removeIndex];
            if (++removeIndex == items.length) {
                removeIndex = 0;
            }
            --count;
            notFull.signal();
            return (T)x;
        } finally {
            lock.unlock();
        }
    }
}

2.2 在ReentrantLock中使用多个Condition实现精细化管理线程

在复杂的同步场景中,可能需要多种条件来控制线程的状态。例如,在一个生产者-消费者模型中,notFull和notEmpty两个条件可以被分别用来控制生产者和消费者的行为。

2.3 结合Condition和StampedLock实现复杂的同步机制

虽然StampedLock不支持条件变量,但我们可以结合Condition和StampedLock来解决一些更复杂的同步场景。这通常意味着需要额外的同步机制,如使用ReentrantLock来实现Condition,同时使用StampedLock来提供乐观读功能。

举报

相关推荐

0 条评论