I'm trying to implement a simple read/write lock for a resource accessed concurrently by multiple threads. The workers randomly try reading or writing to a shared object. When a read lock is set, workers should not be able to write until the lock is released. When a write lock is set, read and write are not permitted. Although my implementation seems to work, I believe it is conceptually wrong.
A read operation taking place should allow for more read operations happening at the same time, resulting in the overall number of reads being larger than the number of writes. My program yields numbers that follow the probability of these operations being performed by a worker.
I feel like my implementation is actually not concurrent at all, but I'm having a hard time identifying the mistake. I would really appreciate being pointed in the right direction.
Main class that dispatches and terminates workers:
class Main {
private static final int THREAD_NUMBER = 4;
public static void main(String[] args) {
// creating workers
Thread[] workers = new Thread[THREAD_NUMBER];
for (int i = 0; i < THREAD_NUMBER; i++) {
workers[i] = new Thread(new Worker(i + 1));
}
System.out.println("Spawned workers: " + THREAD_NUMBER);
// starting workers
for (Thread t : workers) {
t.start();
}
try {
Thread.sleep((long) 10000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// stopping workers
System.out.println("Stopping workers...");
for (Thread t : workers) {
t.interrupt();
}
}
}
The Resource class:
class Resource {
enum ResourceLock {
ON,
OFF
}
private static Resource instance = null;
private ResourceLock writeLock = ResourceLock.OFF;
private ResourceLock readLock = ResourceLock.OFF;
private Resource() {}
public static synchronized Resource getInstance() {
if (instance == null) {
instance = new Resource();
}
return instance;
}
public ResourceLock getWriteLock() {
return writeLock;
}
public ResourceLock getReadLock() {
return readLock;
}
public void setWriteLock() {
writeLock = ResourceLock.ON;
}
public void setReadLock() {
readLock = ResourceLock.ON;
}
public void releaseWriteLock() {
writeLock = ResourceLock.OFF;
}
public void releaseReadLock() {
readLock = ResourceLock.OFF;
}
}
And finally the Worker class:
import java.util.Random;
class Worker implements Runnable {
private static final double WRITE_PROB = 0.5;
private static Random rand = new Random();
private Resource res;
private int id;
public Worker(int id) {
res = Resource.getInstance();
this.id = id;
}
public void run() {
message("Started.");
while (!Thread.currentThread().isInterrupted()) {
performAction();
}
}
private void message(String msg) {
System.out.println("Worker " + id + ": " + msg);
}
private void read() {
synchronized(res) {
while (res.getWriteLock() == Resource.ResourceLock.ON) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
res.setReadLock();
// perform read
try {
Thread.sleep((long) 500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
res.releaseReadLock();
res.notifyAll();
}
message("Finished reading.");
}
private void write() {
synchronized(res) {
while (res.getWriteLock() == Resource.ResourceLock.ON || res.getReadLock() == Resource.ResourceLock.ON) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
res.setWriteLock();
// perform write
try {
Thread.sleep((long) 500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
res.releaseWriteLock();
res.notifyAll();
}
message("Finished writing.");
}
private void performAction() {
double r = rand.nextDouble();
if (r <= WRITE_PROB) {
write();
} else {
read();
}
}
}
The reasoning behind having two separate locks for read and write is that I want to have the ability to atomise both operations and their queries for the lock.
Here is an example of the output I'm getting with a 0.5 write probability:
Spawned workers: 4
Worker 2: Started.
Worker 3: Started.
Worker 1: Started.
Worker 4: Started.
Worker 2: Finished writing.
Worker 4: Finished reading.
Worker 1: Finished writing.
Worker 3: Finished writing.
Worker 1: Finished reading.
Worker 4: Finished writing.
Worker 2: Finished reading.
Worker 4: Finished reading.
Worker 1: Finished reading.
Worker 3: Finished writing.
Worker 1: Finished writing.
Worker 4: Finished writing.
Worker 2: Finished writing.
Worker 4: Finished writing.
Worker 1: Finished reading.
Worker 3: Finished writing.
Worker 1: Finished writing.
Worker 4: Finished reading.
Worker 2: Finished writing.
Stopping workers...
Worker 4: Finished writing.
Worker 1: Finished writing.
Worker 3: Finished reading.
Worker 2: Finished reading.
Help much appreciated.
synchronize
for access, then you have an exclusive lock. You have to build your own locking mechanism from scratch. – Limnsynchronized
block, there can’t be any concurrency. As soon as you move the operation out of thesynchronized
block, it will break as every reader doesreadLock = ResourceLock.OFF
at the end, regardless of how many readers are there. That doesn’t work on the conceptual level, already. You need a counter remembering the number of readers to support multiple readers. Besides that, you should put the logic into theResource
class instead of making it a white-box struct and hoping that the callers implement the logic correctly. – Morganne