ReentrantReadWriteLock 多个读线程

Kug*_*itz 4 java concurrency multithreading locking thread-safety

再会

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

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

writingMethod(){
    lock.writeLock().lock();
    do writing ...
    lock.writeLock().unlock();
}
Run Code Online (Sandbox Code Playgroud)

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

编辑:我试图提出一个 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;
}}
Run Code Online (Sandbox Code Playgroud)

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

Hol*_*ger 5

您的代码已经严重损坏,您不必担心任何性能影响。您的代码不是线程安全的。切勿在可变变量上同步

\n\n
synchronized(accounts[from]){\n    accounts[from] -= amount;\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

该代码执行以下操作:

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

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

\n\n

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

\n\n
\n\n

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

\n\n

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

\n\n
public class Bank {\n    private final AtomicInteger[] accounts;\n    public final ReadWriteLock lock = new ReentrantReadWriteLock();\n    Bank(int numAccounts) {\n        // initialize, keep in mind that this array MUST NOT change\n        accounts=new AtomicInteger[numAccounts];\n        for(int i=0; i<numAccounts; i++) accounts[i]=new AtomicInteger();\n    }\n\n    // Multiple Threads are doing transactions.\n    public void transfer(int from, int to, int amount){\n        final Lock sharedLock = lock.readLock();\n        sharedLock.lock();\n        try {\n            accounts[from].addAndGet(-amount);\n            accounts[to  ].addAndGet(+amount);\n        }\n        finally {\n            sharedLock.unlock();\n        }\n    }\n\n    // Only one thread does summation.\n    public int totalMoney(){\n        int sum = 0;\n        final Lock exclusiveLock = lock.writeLock();\n        exclusiveLock.lock();\n        try {\n            for(AtomicInteger account: accounts)\n                sum += account.get();\n        }\n        finally {\n            exclusiveLock.unlock();\n        }\n        return sum;\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

为了完整起见,我猜会出现这个问题,以下是禁止提取超出可用资金的提款流程:

\n\n
static void safeWithdraw(AtomicInteger account, int amount) {\n    for(;;) {\n        int current=account.get();\n        if(amount>current) throw new IllegalStateException();\n        if(account.compareAndSet(current, current-amount)) return;\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n
\n\n

写完上面的例子后,我记得有一个类AtomicIntegerArray更适合这种任务\xe2\x80\xa6

\n\n
private final AtomicIntegerArray accounts;\npublic final ReadWriteLock lock = new ReentrantReadWriteLock();\n\nBank(int numAccounts) {\n    accounts=new AtomicIntegerArray(numAccounts);\n}\n\n// Multiple Threads are doing transactions.\npublic void transfer(int from, int to, int amount){\n    final Lock sharedLock = lock.readLock();\n    sharedLock.lock();\n    try {\n        accounts.addAndGet(from, -amount);\n        accounts.addAndGet(to,   +amount);\n    }\n    finally {\n        sharedLock.unlock();\n    }\n}\n\n// Only one thread does summation.\npublic int totalMoney(){\n    int sum = 0;\n    final Lock exclusiveLock = lock.writeLock();\n    exclusiveLock.lock();\n    try {\n        for(int ix=0, num=accounts.length(); ix<num; ix++)\n            sum += accounts.get(ix);\n    }\n    finally {\n        exclusiveLock.unlock();\n    }\n    return sum;\n}\n
Run Code Online (Sandbox Code Playgroud)\n