dan b
dan b

Reputation: 1182

Java Read Write Locks on a resource conserving memory

In memory is a large collection of objects of type R. To modify an object requires a write lock and to read requires a read lock. I could store a ReadWriteLock as a private member of the class R, however, I want to conserve memory. At any time, only a small percentage of the objects are being modified or read. There are various ways to decide to not store a read write lock for a particular resource (for example, if it has not be read or written to for some amount of time, t). For purposes of this question assume that periodically it can be determined that the lock for the resource can be deleted. However, keep in mind that while the lock for the resource is being deleted in a thread, one or more other threads may attempt to modify or read the resource. All this is occuring in a multithreaded environment. How would you implement this with the least amount of locking?

For example, one way to do this is to store the read write locks in a concurrent map:

Map<R,ReadWriteLock> map = new ConcurrentHashMap<>();

When it is determined that the read write lock for a resource can be deleted then remove it from the map. However, it may be possible as mentioned above that after it has been decided to delete the entry and before the entry is deleted, that other threads may want to acquire a read or write lock.

You may think that a combination of computeifabsent and remove can be used. However, that does not work. For example:

//--Thread1 write lock--
ReadWriteLock rwl = map.computeIfAbsent(r, r -> new ReadWriteLock()); // 1
rwl.writeLock.lock();                                        // 4
//Modify r here

//--Thread2: Removing entry--
map.remove(r);                                               // 2

//Thread3: write lock
ReadWriteLock rwl = map.computeIfAbsent(r, r-> new ReadWriteLock()); // 3
rwl.writeLock.lock();                                        // 5
//Modify r here.

The problem is that the lock object by thread 1 will not be the same as the lock obtained by thread 3 and incorrectly allowing two writes to occur at the same time. The numbers on the right show the order of execution.

An answer need not use a concurrent map as given in the example above but it seems to be a good start and provides concurrent access to locks. If you do use a concurrent map feel free to wrap the ReadWriteLock in another structure or to create your own version of ReadWriteLock.

In summary the question is how to maintain read write locks for a collection of resources without having to store a read write lock for every object in the collection and minimizing lock contention.

Upvotes: 1

Views: 500

Answers (2)

Malte Hartwig
Malte Hartwig

Reputation: 4555

You can use the methods compute and computeIfPresent to your advantage. The important thing is to do the adding/locking/removing inside the consumers to have it done atomically.

Note: you used putIfAbsent in your example, but that returns the previously asigned value, not the newly assigned value.

public static class Locks<R>
{
    private ConcurrentHashMap<R, ReentrantReadWriteLock> locks = new ConcurrentHashMap<>();

    public void lock(R r, Function<ReentrantReadWriteLock, Lock> whichLock)
    {
        locks.compute(r, (key, lock) -> {
            ReentrantReadWriteLock actualLock = lock == null ? new ReentrantReadWriteLock() : lock;
            whichLock.apply(actualLock).lock();
            return actualLock;
        });
    }

    public void unlock(R r, Function<ReentrantReadWriteLock, Lock> whichLock)
    {
        locks.computeIfPresent(r, (key, lock) -> {
            whichLock.apply(lock).unlock();
            return lock; // you could return null here if lock is unlocked (see cleanUp) to remove it immediately
        });
    }

    public void cleanUp()
    {
        for (R r : new ArrayList<>(locks.keySet()))
        {
            locks.computeIfPresent(r, (key, lock) -> locks.get(r).isWriteLocked()
                                                     || locks.get(r).getReadLockCount() != 0 ? lock : null);
        }
    }
}

Note how I use

  • compute in lock to create new locks and lock them immediately
  • computeIfPresent in unlock to check whether there is a lock at all
  • computeIfPresent in cleanUp to check whether a lock is needed without another thread locking write lock while I am checking read lock count

Right now, unlock is rather useless (except for null checks, which is just a precaution). Returning null in unlock would nicely clean up unnecessary locks and make cleanUp obsolete, but might increase the need for new locks being created. This depends on how frequently locks are used.

You could add convenience methods for read/write, of course, instead of having to provide the getter whichLock.

Upvotes: 1

Ohm&#39;s Lawman
Ohm&#39;s Lawman

Reputation: 393

the question is how to maintain read write locks for a collection of resources without having to store a read write lock for every object in the collection and minimizing lock contention

Have you considered using striped locks? (e.g., https://google.github.io/guava/releases/19.0/api/docs/com/google/common/util/concurrent/Striped.html)

Basically, it's a collection of N locks for M data where N < M. A hash function is used to map the identity of any given datum to the lock that controls it.

Upvotes: 1

Related Questions