Does it even need to be synchronized at all if all of the other threads are only allowed to read it?
Yes. It's commonly thought that thread safety relates only to read-write cycles, but the only assumption you can reliably make without synchronisation is that a thread can at least see its own changes; everything else is maybe-sorta-possibly at best and a politician's promise at worst.
Processor cores may--and often do--take copies of data from main memory to work on. Even if a thread does decide to write through to main memory, the other threads need to catch on that something has changed, and reload it when needed. Otherwise, the other cores will happily chug on with their local copy.
So access should still be controlled, though it doesn't need to be via the synchronized
keyword. A field that is either written independently of its previous value, or written only by the same thread, can be safely guarded by volatile
.
I'd say there are three main ways to accomplish what you want, in varying degrees of features and safety:
// [1] Synchronized, may suffer under heavy contention
class DataSegment {
private Metrics metrics = Metrics.getInstance();
public synchronized String printValues() {
return value1 + morevalues + metrics.cache;
}
}
class Metrics {
// multiple fields
String cache;
Object other;
int stuff;
synchronized void updateMetrics() {
// ...
}
}
// [2] Locks, more complicated than 1, but shared read access
class DataSegment {
private Metrics metrics = Metrics.getInstance();
public String printValues() {
metrics.lockForReading();
try {
return value1 + morevalues + metrics.cache;
} finally {
metrics.unlockForReading();
}
}
}
class Metrics {
private final ReadWriteLock rwlock;
String cache;
Object other;
int stuff;
void lockForReading() {
rwlock.readLock().lock();
}
void unlockForReading() {
rwlock.readLock().unlock();
}
void updateMetrics() {
rwlock.writeLock().lock();
try {
// ...
} finally {
rwlock.writeLock().unlock();
}
}
}
// [3] Bare minimum volatile
class DataSegment {
private Metrics metrics = Metrics.getInstance();
public String printValues() {
return value1 + morevalues + metrics.cache;
}
}
class Metrics {
volatile String cache;
void updateMetrics() {
// ...
}
}
Here's a quick rundown of the ways to synchronize, how to choose, and when to use them:
synchronized is the basic, easiest, least surprising way. It guarantees that any threads entering the block will see all changes threads made before leaving the block. Use it as your first recourse, when you have blocks of code that can not tolerate interference from other threads, such as when doing coordinated or non-atomic reads and writes. JVMs tend to be pretty smart with synchronized, causing little overhead under light contention.
Locks are more specialist tools, allowing finer control than synchronized does. Use locks when you need features that synchronized doesn't provide (like shared/exclusive locking), when your critical sections are not neatly confined to code blocks, or when you have heavy contention.
volatile marks a field as uncacheable, meaning all threads must access main memory to read or write. When a field is volatile, all reads from and writes to (and through) it count as memory barriers, just like an enter-and-leave of a synchronized block does. However, volatile is non-blocking, and threads can interfere with each other. Use volatile when reads and writes are uncoordinated and independent.
theInstance
declared? \$\endgroup\$