Fixed variables can be initialized in the constructor
No methods to change the variable are provided
public class Leader {
private final NodeId n;
public Leader(NodeId n) {
this.n = n;
}
// no setter for field!
}
Immutable Objects: Stateless Methods
Methods that have no bearing on the state of the object
Pure functions
return value depends only on the passed arguments
return value may also depend upon immutable variables
public class NumericalOps {
public static int add(int a,
int b) {
return a + b;
}
}
public class Leader {
private final NodeId n;
//...
public int rank(NodeId k) {
if (k.id() < n.id()) {
return n.id() - k.id();
} else {
return 0;
}
}
}
Immutable Objects: Copying Policy
Making a local temporary copy and returning it as the result
of the computation protects the original object
Copy needs to be atomic in order to avoid destroying data integrity
The object passed as argument needs to offer an atomic copy method
public int[] sort(int[] array) {
int[] copy = new int[array.length];
// place sorted elements in copy...
return copy;
}
Immutable Objects: Records
Java records are a compact way to declare immutable data carriers.
Regular immutable class
public class Leader {
private final NodeId n;
public Leader(NodeId n) {
this.n = n;
}
public NodeId n() { return n; }
@Override
public boolean equals(Object o) { ... }
@Override
public int hashCode() { ... }
@Override
public String toString() { ... }
}
As a record
public record Leader(NodeId n) { }
The compiler generates all of the boilerplate automatically.
Immutable Objects: What Records Provide
For each component declared in the header the compiler generates:
A private final field
A public accessor method named after the component (no get prefix)
A canonical constructor that initializes all fields
equals(), hashCode(), and toString() derived from all components
Leader a = new Leader(someNode);
Leader b = new Leader(someNode);
a.n(); // accessor, note: not getName()
a.equals(b); // true if all components are equal
a.toString(); // "Leader[n=...]"
Immutable Objects: Record Limitations
Implicitly final: records cannot be extended, and cannot extend other classes
they may implement interfaces
No additional instance fields: only the components in the header
All fields are final: no setters, no mutable state
// OK: validation in a compact constructor
public record Leader(NodeId n) {
public Leader {
if (n == null) throw new IllegalArgumentException();
}
}
// NOT OK: adding instance state
public record Leader(NodeId n) {
private int cache; // compile error!
}
Synchronized Objects: Full Synchronization
The goal is to ensure mutual exclusion among all the methods
accessing the object
every method is synchronized
no public fields are present
Access to other objects can break encapsulation!
Careful analysis and discipline is required
Display Room Lighting
Assume that we have a display room with multiple light sources
one and only one light is on at any one time
a user can select a light to turn on
a user can turn off the light, forcing some other light to be turned on
Synchronized Object: Display Room Lighting
public class LightControl {
private List<Light> lights;
private Light onLight;
private Random rand;
// initialize fields...
public synchronized void on(Light light) {
if (onLight != null) onLight.turnOff();
onLight = light;
onLight.turnOn();
}
public synchronized void off() {
if (onLight != null) onLight.turnOff();
onLight = lights.get(rand.nextInt(lights.size()));
onLight.turnOn();
}
}
Static Field Complications
Synchronization assures mutual exclusion among methods of the
same object
Methods of different objects can interfere with each other if
they access static fields
Options:
allow only static synchronized methods to access static fields
use block synchronization with a lock on that class: getClass()
Protecting Static Fields: File Users Counter
public class FileUsers {
private static int userCount = 0; // never access directly
protected void beginUsing() {
synchronized (getClass()) {
++userCount;
}
}
protected void endUsing() {
synchronized (getClass()) {
--userCount;
}
}
public static synchronized int numberUsing() {
return userCount;
}
}
Partial Synchronization
Only methods that can interfere with each other are declared
as synchronized
Only sections of code where interference can occur are
protected by a synchronization block
synchronized(this) {
// code here...
}
Partial Synchronization: Linked List
public class LinkedCell {
protected double value;
protected final LinkedCell next;
public LinkedCell(double value, LinkedCell next) {
this.value = value;
this.next = next;
}
public synchronized double getValue() { return value; }
public synchronized void setValue(double value) {
this.value = value;
}
public LinkedCell getNext() { return next; }
// add up element values
public double sum() {
double v = getValue(); // get value via synchronized accessor
if (next() != null) { v += next().sum(); }
return v;
}
}
value, setValue, and sum share access to value, access must be synchronized!
Contained Objects: Exclusive Ownership
The outer object
ensures that only one thread executes at a time (synchronized methods)
subordinate objects are locally created
references to the subordinate objects are not leaked
not passed as arguments
not passed as returned values
Exclusive Ownership Example
Contained Objects: Managed Ownership
It is often the case that ownership of a resource may change over time
Invariant: only one accessible reference exists in the system at any one time
Ownership transfer operations must be subject to defined policies
enforced by design
Liveness: Failure Modes
Reliance on timing properties
proofs of concurrent programs are meant to show that under
all possible interleaving of events the execution is correct
Contention on the CPU leading to starvation
proofs of concurrent programs assume that no thread is
denied service by the OS
Dormancy: a suspended thread never becomes schedulable again
suspend/resume and wait/notify pairing errors
Premature termination
Deadlock
Deadlock Prevention
Every thread locks the resources it needs in the same order
The locking order is not always readily visible
Complications arise when locks are deep inside the object
nesting structure
public class Document {
private Document otherPart;
public synchronized void print() {
// print this part of the document
}
public synchronized void printAll() {
otherPart.print();
print();
}
}
Deadlock Illustration
Deadlock Illustration
Deadlock Illustration
Additional Failure Modes
Data integrity violations
synchronized methods do not always guarantee atomicity
data inconsistencies lead to incorrect decisions
Lack of progress
progress made by one thread is undone by another
Livelock
threads take turns yielding to each other
Livelock Illustration
Livelock Illustration
Livelock Illustration
Livelock Illustration
Livelock Illustration
How could we stop colliding?
Locking and Performance
Synchronized methods and locks may introduce performance penalties
correctness must come first
thoughtful analysis can ensure correctness and enhance performance
Instance variable analysis helps determine where synchronization
is or is not needed
Proper design of object hierarchies may simplify analysis and
reduce the need for synchronization
Interleaving Semantics
Access and update methods are normally synchronized
A very long update can delay reading of the data
Two solutions:
employ synchronization blocks
let the access method be un-synchronized as long as it
returns one of only two values:
the value before the update
the value after the update
A Conservative Design Revisited
All methods of Monitor are synchronized
protection against any data inconsistencies
Only one thread can use Monitor at a time
Class Splitting
Monitor can be redesigned
by using synchronization only on contained objects that need it
Multiple threads can use Monitor concurrently
More generally, contained objects may be designed in order to
achieve fine-grained synchronization and increased levels of
concurrency
Class Splitting
Lock Splitting
Every Java Object can be a lock
Synchronized methods use the object to which they belong as a lock
Another object could be used to accomplish the same objective
Using multiple locks allows subgroups of methods to be
mutually exclusive
Going one step further, locks can be selectively used in a
state dependent manner