【问题标题】:ReentrantReadWriteLock multiple reading threadsReentrantReadWriteLock 多个读取线程
【发布时间】:2015-06-16 09:14:26
【问题描述】:

美好的一天

我有一个关于 ReentrantReadWriteLocks 的问题。我正在尝试解决一个问题,即多个读取器线程应该能够在数据结构上并行操作,而一个写入器线程只能单独操作(而没有读取器线程处于活动状态)。我正在使用 Java 中的 ReentrantReadWriteLocks 来实现这一点,但是从时间测量来看,阅读器线程似乎也在相互锁定。我认为这不应该发生,所以我想知道我是否执行错了。我的实现方式如下:

readingMethod(){
    lock.readLock().lock();
    do reading ...
    lock.readLock().unlock();
}

writingMethod(){
    lock.writeLock().lock();
    do writing ...
    lock.writeLock().unlock();
}

读取方法被许多不同的线程调用。从测量时间来看,读取方法是按顺序执行的,即使写入方法从未被调用!关于这里出了什么问题的任何想法?提前谢谢你-干杯

编辑:我试图提出一个 SSCCE,我希望这很清楚:

public class Bank {
private Int[] accounts;
public ReadWriteLock lock = new ReentrantReadWriteLock();

// Multiple Threads are doing transactions.
public void transfer(int from, int to, int amount){
    lock.readLock().lock(); // Locking read.

    // Consider this the do-reading.
    synchronized(accounts[from]){
        accounts[from] -= amount;
    }
    synchronized(accounts[to]){
        accounts[to] += amount;
    }

    lock.readLock().unlock(); // Unlocking read.
}

// Only one thread does summation.
public int totalMoney(){
    lock.writeLock().lock; // Locking write.

    // Consider this the do-writing.
    int sum = 0;
    for(int i = 0; i < accounts.length; i++){
        synchronized(accounts[i]){
            sum += accounts[i];
        }
    }

    lock.writeLock().unlock; // Unlocking write.

    return sum;
}}

我知道读锁内部的部分实际上不是读而是写。我这样做是因为有多个线程执行写入,而只有一个线程执行读取,但是在读取时,不能对数组进行任何更改。这在我的理解中有效。再说一遍,read-Locks 中的代码可以在多个线程中正常工作,只要不添加 write 方法和 read-locks。

【问题讨论】:

  • 您的机器上有多少个 CPU 可用?
  • 这里没有足够的信息来提供有意义的帮助。这可能是多种原因;简单/幼稚的基准测试技术会在 Java 中产生不一致的结果(正确地对 Java 进行微基准测试确实不容易),您可能在“阅读”中的额外同步中遇到瓶颈,“阅读”的复杂性可能如此之低,以至于线程在它们的时间片内完成,观察到的顺序行为实际上只是上下文切换等的开销。尝试提供一个清楚地证明问题的 SSCCE。
  • 如果你说有额外的synchronized 块,你已经自己回答了这个问题……
  • @Holger 经过一番测试,我现在发现了以下几点: 线程以正确的方式进入和退出读取方法(在 lock 和 unlock 语句内),并且不会相互锁定。然而,一个线程完成一次运行该方法所需的时间明显高于没有锁定/解锁的时间。是否有可能,读写锁也出于某种原因阻止所有其他使用的锁,即使它们不是同一个锁对象?我已经在数据结构的不同元素上同步了做阅读,所以不是同一个锁。
  • 通常,使用显式锁的理由是获得您所追求的那种控制。同步块使用 Java 中所有对象都具有的隐式锁。如果你所有的读者都在调用 readMethod(),这意味着他们首先从你的 ReadWriteLock 获取读锁,然后获取同步块对象上的隐式锁。由于您没有使用真正的 SSCCE 编辑您的问题,因此只能假设您正在有效地创建一个双重锁,其中内部锁阻止读者同时访问相同的数据结构。

标签: java multithreading concurrency thread-safety locking


【解决方案1】:

您的代码被严重破坏,您不必担心任何性能影响。您的代码不是线程安全的。 永远不要在可变变量上同步!

synchronized(accounts[from]){
    accounts[from] -= amount;
}

此代码执行以下操作:

  • 在没有任何同步的情况下读取from 位置处的数组accounts 的内容,因此可能读取了一个过时的值,或者一个仍在其synchronized 块内的线程正在写入的值
  • 锁定已读取的任何对象(请记住,Integer 对象 created by auto-boxing 的身份未指定 [-128 到 +127 范围除外])
  • 在from位置再次读取数组accounts的内容
  • 从其int 值中减去amount,自动装箱结果(在大多数情况下产生一个不同 对象)
  • 将新对象存储在数组accounts的位置from

这意味着不同的线程可以同时写入相同的数组位置,同时锁定在其第一次(非同步)读取时发现的不同 Integer 实例,从而增加了数据竞争的可能性。

这也意味着线程可能会在不同的数组位置上相互阻塞,如果这些位置恰好具有相同的值并且恰好由相同的实例表示。例如。用零值(或全部为 -128 到 +127 范围内的相同值)预初始化数组是接近单线程性能的好方法,因为零(或这些其他小值)是少数 @ 之一987654336@ 值保证由同一个实例表示。由于您没有体验过NullPointerExceptions,因此您显然已经用一些东西预先初始化了数组。


总而言之,synchronized 适用于对象实例,而不是变量。这就是为什么在尝试对 int 变量执行此操作时无法编译的原因。由于在不同对象上进行同步就像根本没有任何同步,所以您永远不应该在可变变量上进行同步。

如果您想要线程安全的并发访问不同的帐户,您可以使用AtomicIntegers。这样的解决方案将为每个帐户使用一个永远不会改变的 AtomicInteger 实例。仅使用其线程安全方法更新其余额值。

public class Bank {
    private final AtomicInteger[] accounts;
    public final ReadWriteLock lock = new ReentrantReadWriteLock();
    Bank(int numAccounts) {
        // initialize, keep in mind that this array MUST NOT change
        accounts=new AtomicInteger[numAccounts];
        for(int i=0; i<numAccounts; i++) accounts[i]=new AtomicInteger();
    }

    // Multiple Threads are doing transactions.
    public void transfer(int from, int to, int amount){
        final Lock sharedLock = lock.readLock();
        sharedLock.lock();
        try {
            accounts[from].addAndGet(-amount);
            accounts[to  ].addAndGet(+amount);
        }
        finally {
            sharedLock.unlock();
        }
    }

    // Only one thread does summation.
    public int totalMoney(){
        int sum = 0;
        final Lock exclusiveLock = lock.writeLock();
        exclusiveLock.lock();
        try {
            for(AtomicInteger account: accounts)
                sum += account.get();
        }
        finally {
            exclusiveLock.unlock();
        }
        return sum;
    }
}

为了完整起见,我猜这个问题会出现,下面是一个禁止多取钱的取款流程可能如下所示:

static void safeWithdraw(AtomicInteger account, int amount) {
    for(;;) {
        int current=account.get();
        if(amount>current) throw new IllegalStateException();
        if(account.compareAndSet(current, current-amount)) return;
    }
}

可以通过将accounts[from].addAndGet(-amount); 替换为safeWithdraw(accounts[from], amount); 来包含它。


在写完上面的例子之后,我记得有一个类 AtomicIntegerArray 更适合这种任务......

private final AtomicIntegerArray accounts;
public final ReadWriteLock lock = new ReentrantReadWriteLock();

Bank(int numAccounts) {
    accounts=new AtomicIntegerArray(numAccounts);
}

// Multiple Threads are doing transactions.
public void transfer(int from, int to, int amount){
    final Lock sharedLock = lock.readLock();
    sharedLock.lock();
    try {
        accounts.addAndGet(from, -amount);
        accounts.addAndGet(to,   +amount);
    }
    finally {
        sharedLock.unlock();
    }
}

// Only one thread does summation.
public int totalMoney(){
    int sum = 0;
    final Lock exclusiveLock = lock.writeLock();
    exclusiveLock.lock();
    try {
        for(int ix=0, num=accounts.length(); ix<num; ix++)
            sum += accounts.get(ix);
    }
    finally {
        exclusiveLock.unlock();
    }
    return sum;
}

【讨论】:

  • 非常感谢这个非常详细的答案!当然,我可以在我的示例中搞砸,试图使其尽可能通用和紧凑。在我的代码中,我为每个帐户都有一个单独的类/对象,并且该数组不会保存整数值,而是“Account.class”的实例,这在并发设置中当然可以。然而,我面临的问题仍然没有被这个实现解决(至少根据快速测试)。但是我不确定我现在是否应该编辑这个问题,因为您在这里编译的答案对其他人具有如此学习价值!
  • 我同意过多的编辑会降低问题的价值。 Account 类可能是一个完全不同的问题。例如。有没有办法使更新成为原子操作?也许你想开一个新问题,然后用Account和Bank的完整代码...
  • 这可能是可能的,尽管我不确定如何。它只包含一个持有余额的整数,以及两个同步方法“getBalance”和“setBalance”。如果解决这个问题的方法不是很简单,我会考虑提出一个新问题。
  • 您将需要一个类似于AtomicInteger 的 API,例如添加一个类似changeBalance 的方法,它将差异作为参数。通常,您需要将所有操作作为原子实现提供,否则需要使用getBalance 和setBalance 进行复合。我的回答中的safeWithdraw 方法显示了如何将任意get-process-set 操作实现为原子操作的一般模式。
  • 我想我明白你的意思了,但是提供原子操作setBalance(以你的safeWithdraw 实现方式)实际上等同于将其实现为同步方法吗?
【解决方案2】:

您可以在此测试中运行 2 个线程

static ReadWriteLock l = new ReentrantReadWriteLock();

static void readMehod() {
    l.readLock().lock();
    System.out.println(Thread.currentThread() + " entered");
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    l.readLock().unlock();
    System.out.println(Thread.currentThread() + " exited");
}

并查看两个线程是否进入读锁。

【讨论】:

  • 好的,我这样做了,似乎所有线程(用 2 和 12 都尝试过)都以正确的方式进入和退出,而不会相互阻塞。似乎问题出在其他地方..
  • 我在上面的评论中回答了“Holger”,我在这里也提到了这一点。只是想让你知道..也许你对此有所了解
猜你喜欢
  • 1970-01-01
  • 2016-03-05
  • 1970-01-01
  • 2015-03-18
  • 2013-04-24
  • 1970-01-01
  • 1970-01-01
  • 2013-08-20
  • 1970-01-01
相关资源
最近更新 更多