4

Good Day

I have a question relating ReentrantReadWriteLocks. I am trying to solve a problem where multiple reader threads should be able to operate in parallel on a data structure, while one writer thread can only operate alone (while no reader thread is active). I am implementing this with the ReentrantReadWriteLocks in Java, however from time measurement it seems that the reader threads are locking each other out aswell. I don't think this is supposed to happen, so I am wondering if I implemented it wrong. The way I implemented it is as follows:

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

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

Where the reading method is called by many different threads. From measuring the time, the reading method is being executed sequentially, even if the writing method is never invoked! Any Idea on what is going wrong here? Thank you in advance -Cheers

EDIT: I tried to come up with a SSCCE, I hope this is clear:

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;
}}

I know the parts inside the read-Lock are not actually reads but writes. I did it this way because there are multiple threads performing writes, while only one thread performs reads, but while reading, no changes can be made to the array. This works in my understanding. And again, the code inside the read-Locks works fine with multiple threads, as long as no write method and no read-locks are added.

Kugelblitz
  • 582
  • 5
  • 24
  • How many CPUs are available on your machine? – Andrew Henle Apr 10 '15 at 15:19
  • @AndrewHenle 6 Cores, 12 Hyperthreads. So 12 Processing Cores for the JVM – Kugelblitz Apr 10 '15 at 15:20
  • Try saving System.nanoTime() before the lock and after the unlock, then comparing the results from different threads. I'm assuming System.nanoTime() provides consistent enough data in different threads for this to be useful. – Andrew Henle Apr 10 '15 at 15:28
  • @AndrewHenle The problem is that inside the reading portion and the writing portion, there are additional synchronized blocks which make time measurements for individual threads kind of unconsistent, since they vary vastly. If you think this still helps I can of course try it, but since the program pretty much only consists of those two functions, I know from measuring the total time and comparing it to T1 (1 thread) it is slightly higher than that, so no improvement. – Kugelblitz Apr 10 '15 at 15:33
  • It can't hurt. It would at least show if the read/write lock is serializing reads. You could also do that by starting up one or more threads that get a read lock then just hold it and see if other threads block trying to get a read lock. – Andrew Henle Apr 10 '15 at 15:36
  • 2
    There is not enough information here to provide meaningful help. It could be any number of reasons; simple/naïve benchmarking techniques will produce inconsistent results in Java (it's really not easy to microbenchmark Java properly), you may have a bottleneck in the additional synchronization in "do reading", the complexity of "do reading" could be so low that the threads finish within their time slice and the observed sequential behavior is really just overhead from context switching, etc. Try to provide an SSCCE that clearly demonstrates the problem. – Andreas Troelsen Apr 10 '15 at 15:42
  • @AndreasTroelsen Working on it, more info coming! – Kugelblitz Apr 10 '15 at 15:53
  • 1
    If you say that there are additional `synchronized` blocks, you already have answered the question yourself… – Holger Apr 10 '15 at 16:07
  • @AndreasTroelsen Ok, so to clarify: I know what you mean, but the synchronized blocks inside the do read are not a bottleneck. I had the program consist of only the first method at first, with only the do read part (with synchronized blocks) and had it run in parallel and sequentially. The parallel, synchronized way was a huge improvement. However, as soon as I added just those two statements to lock and unlock the read lock at the beginning and end of the do reading part, all the speedup was gone and the total time was equal to T1 again. So no, the part inside do reading is not a bottleneck.. – Kugelblitz Apr 10 '15 at 16:13
  • @Holger Do you mean that, which I tried to clarify in the above comment? – Kugelblitz Apr 10 '15 at 16:15
  • Just use a profiler to find out where the threads spend their time… – Holger Apr 10 '15 at 16:19
  • 1
    @Holger After a bit of testing I have now figured the following: The Threads enter and exit the read Method (inside the lock and unlock statement) in a correct way and do not lock each other out. The time it takes for one thread to finish with one run through the method is however significantly higher than without the lock / unlock. Is it possible, that the ReadWrite Lock also for some reason blocks ALL other locks used, even if they are not the same lock-object? I have synchronized inside the do reading, on different elements of the data structure though, so not the same lock. – Kugelblitz Apr 10 '15 at 16:53
  • 1
    Typically, the argument for using explicit locks is to gain the kind of control you are after. Synchronized blocks use the implicit lock that all objects in Java have. If all your readers are calling readMethod(), this means that they first acquire the read lock from your ReadWriteLock, and then acquire the implicit lock on the object of the synchronized block. Since you haven't edited your question with a true SSCCE, one can only assume that you are effectively creating a double-lock where the inner lock prevents the readers from accessing the same datastructure concurrently. – Andreas Troelsen Apr 10 '15 at 23:12

2 Answers2

4

Your code is so horribly broken that you should not worry about any performance implication. Your code is not thread safe. Never synchronize on a mutable variable!

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

This code does the following:

  • read the contents of the array accounts at position from without any synchronization, thus possibly reading a hopelessly outdated value, or a value just being written by a thread still inside its synchronized block
  • lock on whatever object it has read (keep in mind that the identity of Integer objects created by auto-boxing is unspecified [except for the -128 to +127 range])
  • read again the contents of the array accounts at position from
  • subtract amount from its int value, auto-box the result (yielding a different object in most cases)
  • store the new object in array accounts at position from

This implies that different threads can write to the same array position concurrently while having a lock on different Integer instances found on their first (unsynchronized) read, opening the possibility of data races.

It also implies that threads may block each other on different array positions if these positions happen to have the same value happened to be represented by the same instance. E.g. pre-initializing the array with zero values (or all to the same value within the range -128 to +127) is a good recipe for getting close to single thread performance as zero (or these other small values) is one of the few Integer values being guaranteed to be represented by the same instance. Since you didn’t experience NullPointerExceptions, you obviously have pre-initialized the array with something.


To summarize, synchronized works on object instances, not variables. That’s why it won’t compile when trying to do it on int variables. Since synchronizing on different objects is like not having any synchronization at all, you should never synchronize on mutable variables.

If you want thread-safe, concurrent access to the different accounts, you may use AtomicIntegers. Such a solution will use exactly one AtomicInteger instance per account which will never change. Only its balance value will be updated using its thread-safe methods.

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;
    }
}

For completeness, as I guess this question will arise, here is how a withdraw process forbidding taking more money than available may look like:

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;
    }
}

It may be included by replacing the line accounts[from].addAndGet(-amount); by safeWithdraw(accounts[from], amount);.


Well after writing the example above, I remembered that there is the class AtomicIntegerArray which fits even better to this kind of task…

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;
}
Holger
  • 285,553
  • 42
  • 434
  • 765
  • Huge thanks for this very detailed answer! Of course I was able to mess up in my example in trying to make it as general and compact as possible. In my code I have a seperate Class/Object for each and every account, and the array would not hold integer values, but instances of "Account.class" instead, which of course would be ok in a concurrency setting. The problem I was facing however is still not solved by this implementation (at least according to a quick test). Yet I'm not sure if I should even edit the question now, since the answer you compiled here is of such learning value for others! – Kugelblitz Apr 14 '15 at 17:42
  • 1
    I agree that too much editing would reduce the value of the question. The `Account` class may be an entirely different issue. E.g. is there a way to make the update an atomic operation? Maybe you want to open a new question, then with the complete code of `Account` and `Bank`… – Holger Apr 14 '15 at 17:47
  • That might be possible, allthough I am not certain how. It only contains an integer holding the balance, and two synchronized methods "getBalance" and "setBalance". If the solution to this is not a really trivial one I will conside opening a new question. – Kugelblitz Apr 14 '15 at 17:55
  • 1
    You will need an API similar to that of `AtomicInteger`, e.g. add a method like `changeBalance` which takes a difference as parameter. Generally, you’ll need to offer all operations as atomic implementations which otherwise need to be compound using `getBalance` and `setBalance`. The `safeWithdraw` method of my answer shows the general pattern of how to implement arbitrary get-process-set operations as atomic. – Holger Apr 15 '15 at 07:43
  • I think I see what you mean, but wouldn't presenting an atomic operation `setBalance` (in the way of your `safeWithdraw` implementation) be virtually equivalent to implementing it as a synchronized method? – Kugelblitz Apr 15 '15 at 08:22
  • If `setBalance` consist of a single write to the `int` variable, it is already atomic, and declaring the `int` variable as `volatile` would be enough to make it thread-safe. However, to enable atomic get-process-set operations like `safeWithdraw`, you need a *cas* (atomic *compare-and-set*) operation; that’s the key feature. You can provide it by either, convert your `int` variable to an `AtomicInteger` or declare it as `volatile` and use an [`AtomicIntegerFieldUpdater`](http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/atomic/AtomicIntegerFieldUpdater.html) to update the field. – Holger Apr 15 '15 at 08:35
  • This is getting rather confusing without being able to provide additional code info from my part I think. So what I did now is to change the `balance` field to volatile, but I don't quite understand how to use the _cas_. I am strongly considering opening a new question where I can provide my code, but I am worried it might get taken as a duplication for this one. – Kugelblitz Apr 15 '15 at 08:46
  • I composed a new question, in case you were still interested. – Kugelblitz Apr 15 '15 at 09:46
1

You can run 2 threads on this test

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");
}

and see if both threads enter the readlock.

Evgeniy Dorofeev
  • 133,369
  • 30
  • 199
  • 275
  • Ok so I did this, and it seems like all threads (tried it with both 2 and 12) are entering and exiting in a correct manner, without blocking each other. Seems like the problem lies somewhere else.. – Kugelblitz Apr 10 '15 at 16:39
  • I answered to "Holger" in a comment above, where I also mention this here. Just to let you know.. Maybe you know something about that – Kugelblitz Apr 10 '15 at 16:56