I'd like to design a settings class thread save. The settings have 2 attributes: String x
and int y
and should provide listener functionality to notify listener about changes. The problem is, how to make it thread safe.
Here is a complete code:
package org.example;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public final class Settings {
private static final String DEFAULT_X = "<DEFAULT>";
private static final int DEFAULT_Y = -1;
public interface IListener {
public void xChanged(final String newX, final String oldX);
public void yChanged(final int newY, final int oldY);
}
// No access control via lock needed
private final Set<IListener> listeners = new CopyOnWriteArraySet<>();
/** Lock for read and write acces to {@link #x} and {@link #y} */
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private String x = DEFAULT_X;
private int y = DEFAULT_Y;
public void addListener(final IListener listener) {
if (listener == null) {
throw new NullPointerException();
}
listeners.add(listener);
}
public void removeListener(final IListener listener) {
if (listener == null) {
throw new NullPointerException();
}
listeners.remove(listener);
}
public void copyFrom(final Settings origin) {
if (origin == null) {
throw new NullPointerException();
}
assert origin.getX() != null;
lock.writeLock().lock();
setX(origin.getX());
setY(origin.getY());
lock.writeLock().unlock();
}
public String getX() {
lock.readLock().lock();
final String x = this.x;
lock.readLock().unlock();
return x;
}
public void setX(final String newX) {
if (newX == null) {
throw new NullPointerException();
}
lock.writeLock().lock();
if (!this.x.equals(newX)) {
final String oldX = this.x;
this.x = newX;
lock.writeLock().unlock();
// lock is released before the listeners get notified
notifyXChanged(newX, oldX);
} else {
// nothing was changed
lock.writeLock().unlock();
}
}
public void setY(final int newY) {
lock.writeLock().lock();
if (this.y != newY) {
final int oldY = this.y;
this.y = newY;
lock.writeLock().unlock();
// lock is released before the listeners get notified
notifyYChanged(newY, oldY);
} else {
// nothing was changed
lock.writeLock().unlock();
}
}
public int getY() {
lock.readLock().lock();
final int y = this.y;
lock.readLock().unlock();
return y;
}
private void notifyXChanged(final String newX, final String oldX) {
for (final IListener listener : listeners) {
listener.xChanged(newX, oldX);
}
}
private void notifyYChanged(final int newY, final int oldY) {
for (final IListener listener : listeners) {
listener.yChanged(newY, oldY);
}
}
}
x
and y
are acess controlled, becouse getX
, getY
uses the read lock and setX
, setY
uses the write lock.
The interesting part is the copyFrom
method. It must be atomic, i. e. x
and y
must set atomically. To get it atomically, I've wrapped it in a lock
-unlock
call:
lock.writeLock().lock();
setX(origin.getX());
setY(origin.getY());
lock.writeLock().unlock();
The problem is, that the listeners methods xChanged
, yChanged
are called so external code is executed. What about the listener call getY
in xChanged
?:
settings.addListener(new IListener() {
@Override
public void yChanged(int newY, int oldY) { }
@Override
public void xChanged(String newX, String oldX) {
System.out.println("y: " + settings.getY());
}
});
The write lock holds (from copyFrom
), so the listener thread is blocked to aqquire the read lock (from getX
). The result is a dead lock I think.
Another possible failure is, that the listener can throw an exception. When an exception is not handled, copyFrom
returns abrupt without releasing the write lock. If is it handled (via try-catch), what should I do with this exception?
Are there best practices to handle such stateful classes with listener functionality (I have the same problem for a finite state machine etc.)?