Appearance
Synchronized Methods
We know that Java programs rely on synchronized
to synchronize threads. When using synchronized
, it is crucial to determine which object to lock.
Allowing threads to choose their own lock objects can lead to confusing code logic and hinder encapsulation. A better approach is to encapsulate the synchronized
logic within the methods themselves. For example, consider the following Counter
class:
java
public class Counter {
private int count = 0;
public void add(int n) {
synchronized(this) {
count += n;
}
}
public void dec(int n) {
synchronized(this) {
count -= n;
}
}
public int get() {
return count;
}
}
By doing this, threads calling the add()
and dec()
methods do not need to worry about the synchronization logic, as the synchronized
blocks are handled internally within these methods. Additionally, notice that the synchronized
keyword locks on this
, meaning the current instance. This allows multiple Counter
instances to operate independently and concurrently without interference:
java
var c1 = new Counter();
var c2 = new Counter();
// Threads operating on c1:
new Thread(() -> {
c1.add(1);
}).start();
new Thread(() -> {
c1.dec(1);
}).start();
// Threads operating on c2:
new Thread(() -> {
c2.add(1);
}).start();
new Thread(() -> {
c2.dec(1);
}).start();
Now, the Counter
class is thread-safe, meaning it can be safely used in a multithreaded environment. Java's standard library class java.lang.StringBuffer
is also thread-safe.
There are other immutable classes, such as String
, Integer
, and LocalDate
, where all member variables are final
. In a multithreaded context, these classes are thread-safe because threads can only read but not modify them. Similarly, classes like Math
, which only provide static methods without member variables, are also thread-safe.
Note: Unless explicitly stated otherwise, a class is not thread-safe by default.
Analyzing the Counter
Class
Let’s examine the Counter
class:
java
public class Counter {
public void add(int n) {
synchronized(this) { // Locking on this
count += n;
} // Unlocking
}
// ...
}
When we lock on the this
instance, it is equivalent to declaring the method as synchronized
. The following two method implementations are equivalent:
Implementation 1: Using synchronized
Block
java
public void add(int n) {
synchronized(this) { // Lock on this
count += n;
} // Unlock
}
Implementation 2: Using synchronized
Method
java
public synchronized void add(int n) { // Lock on this
count += n;
} // Unlock
Therefore, a method marked with synchronized
ensures that the entire method is treated as a synchronized block, locking on the current instance (this
).
Synchronizing Static Methods
Consider what happens if we add the synchronized
keyword to a static method:
java
public synchronized static void test(int n) {
// ...
}
For static methods, there is no this
instance because static methods belong to the class, not to any particular instance. However, every class has a Class
instance automatically created by the JVM. Therefore, synchronizing a static method locks on the class's Class
instance. The above synchronized static
method is effectively equivalent to:
java
public class Counter {
public static void test(int n) {
synchronized(Counter.class) {
// ...
}
}
}
Example: Synchronized get()
Method
Let’s examine the get()
method in the Counter
class:
java
public class Counter {
private int count;
public int get() {
return count;
}
// ...
}
This method does not need synchronization because reading a single int
variable does not require synchronization.
However, if we modify the code to return an object containing two int
variables:
java
public class Counter {
private int first;
private int last;
public Pair get() {
Pair p = new Pair();
p.first = first;
p.last = last;
return p;
}
// ...
}
Synchronization becomes necessary to ensure that both first
and last
are read consistently. Without synchronization, another thread might modify first
or last
while get()
is executing, leading to inconsistent state.
Summary
Synchronized Methods: Using the
synchronized
keyword on a method makes the entire method a synchronized block, locking onthis
for instance methods and on the class'sClass
instance for static methods.Thread Safety through Encapsulation: By encapsulating
synchronized
blocks within methods, you ensure that the synchronization logic is maintained internally, promoting cleaner and safer code.Default Thread Safety: Unless explicitly specified, a class is not thread-safe.
Synchronization on the Same Lock Object: When using
synchronized
, it is crucial to lock on the same object to ensure mutual exclusion. Using different lock objects can lead to data inconsistency as threads can enter their respective synchronized blocks simultaneously.Immutable and Static Classes: Immutable classes (e.g.,
String
,Integer
,LocalDate
) and classes with only static methods (e.g.,Math
) are inherently thread-safe and do not require synchronization.Consistent Variable Access: Methods that read or write multiple shared variables must synchronize access to ensure that the variables are read and written atomically, preventing logical errors.
Equivalence of
synchronized
Blocks and Methods: Locking onthis
using asynchronized
block within a method is equivalent to declaring the entire method assynchronized
.Static Method Synchronization: Synchronizing static methods locks on the class’s
Class
instance, ensuring that only one thread can execute any synchronized static method at a time across all instances.
By following these principles, you can design thread-safe classes and manage synchronized access to shared resources effectively in your Java programs.