Chapter 2 – Common Java Multithreading Utilities and the synchronized Keyword
This chapter introduces advanced Java multithreading concepts, explaining thread safety, the synchronized keyword, various synchronization techniques, and practical code examples such as object‑level, class‑level, method‑level synchronization and double‑checked locking to help solve concurrency problems in performance testing.
In this chapter we explore advanced Java multithreading concepts, focusing on thread safety, synchronization mechanisms, and common utility classes that help resolve concurrency issues encountered during performance testing.
Thread Safety
Thread safety is the most important core issue in Java multithreading. A short story about three friends (Xiao Ba, Xiao Qi, and Xiao Qi 7) illustrates how a change in one thread’s state may not be immediately visible to other threads, leading to inconsistent results.
In Java, when one thread modifies an object’s state, other threads may still hold the old state, causing unexpected behavior. Two intuitive solutions are to place the participants in the same chat room (shared state) or to use separate communication channels (isolated state), which correspond to using thread‑safe classes to synchronize state or converting multithreaded execution to single‑threaded execution.
synchronized Keyword
The synchronized keyword is the most direct and simple tool for solving Java thread‑safety problems.
2.2.1 Basic Syntax
The basic syntax creates a synchronized block that locks on a given object:
Object object = new Object();
synchronized (object) {
// synchronized code
doSomething();
}Another common form synchronizes on the class object:
synchronized (SynchronizedDemo.class) {
// synchronized code
doSomething();
}When you synchronize on an object, the JVM creates a monitor that allows only one thread to execute the block at a time, ensuring thread safety for that object’s state.
Synchronizing on a class’s Class object provides a lock that is shared across all instances, useful for protecting static resources such as counters or caches.
2.2.2 Synchronizing on an Object Instance
Example 1 demonstrates that synchronizing on each instance does **not** guarantee safety when multiple instances are used:
package org.funtester.performance.books.chapter02.section2;
public class SynchronizedDemoFirst {
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
SynchronizedDemoFirst demoFirst = new SynchronizedDemoFirst();
new Thread(() -> {
demoFirst.test();
}).start();
}
}
public void test() {
synchronized (this) {
try { Thread.sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); }
System.out.println(System.currentTimeMillis() + " Hello FunTester! " + Thread.currentThread().getName());
}
}
}All three threads print the same timestamp, showing that each thread locked a different instance and therefore the code was not thread‑safe.
Example 2 fixes the problem by sharing a single instance among all threads:
package org.funtester.performance.books.chapter02.section2;
public class SynchronizedDemoSecond {
public static void main(String[] args) {
SynchronizedDemoSecond first = new SynchronizedDemoSecond();
for (int i = 0; i < 3; i++) {
new Thread(() -> {
first.test();
}).start();
}
}
public void test() {
synchronized (this) {
try { Thread.sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); }
System.out.println(System.currentTimeMillis() + " Hello FunTester! " + Thread.currentThread().getName());
}
}
}Because all threads synchronize on the same this object, the timestamps differ by roughly 100 ms, confirming thread safety.
2.2.3 Synchronizing on a Class Object
Example 3 synchronizes on a static shared object:
package org.funtester.performance.books.chapter02.section2;
public class SynchronizedDemoThird {
static Object object = new Object();
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
SynchronizedDemoThird demo = new SynchronizedDemoThird();
new Thread(() -> {
demo.test();
}).start();
}
}
public void test() {
synchronized (object) {
try { Thread.sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); }
System.out.println(System.currentTimeMillis() + " Hello FunTester! " + Thread.currentThread().getName());
}
}
}All threads lock the same static object**, achieving thread safety across multiple instances.
2.2.4 Synchronizing on a Class’s Class Object
Example 4 synchronizes on the Class object of another class:
package org.funtester.performance.books.chapter02.section2;
public class SynchronizedDemoFoutth {
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
SynchronizedDemoFoutth demo = new SynchronizedDemoFoutth();
new Thread(() -> {
demo.test();
}).start();
}
}
public void test() {
synchronized (SynchronizedDemoFirst.class) {
try { Thread.sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); }
System.out.println(System.currentTimeMillis() + " Hello FunTester! " + Thread.currentThread().getName());
}
}
}This also guarantees that all threads contend for the same lock, regardless of the instance they belong to.
2.2.5 Synchronized Methods
When the synchronized modifier is placed on a method, the lock is implicitly the instance ( this ) for instance methods or the class object for static methods.
Example of a synchronized instance method (equivalent to synchronized(this) ):
public class SynchronizedDemoFifth {
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
SynchronizedDemoFifth demo = new SynchronizedDemoFifth();
new Thread(() -> demo.test()).start();
}
}
public synchronized void test() {
try { Thread.sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); }
System.out.println(System.currentTimeMillis() + " Hello FunTester! " + Thread.currentThread().getName());
}
}Example of a synchronized static method (equivalent to synchronized(ClassName.class) ):
public class SynchronizedDemoSixth {
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
new Thread(() -> test()).start();
}
}
public static synchronized void test() {
try { Thread.sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); }
System.out.println(System.currentTimeMillis() + " Hello FunTester! " + Thread.currentThread().getName());
}
}Both forms ensure that only one thread can execute the method at a time, but they are less flexible and have higher performance overhead compared to fine‑grained synchronized blocks.
2.2.6 Best Practice – Double‑Checked Locking
The double‑checked locking pattern uses a synchronized block on the class’s Class object to lazily initialize a singleton while minimizing synchronization cost:
public class DoubleCheckedLocking {
private static DoubleCheckedLocking driver;
public static DoubleCheckedLocking getDriver() {
if (driver == null) {
synchronized (DoubleCheckedLocking.class) {
if (driver == null) {
driver = new DoubleCheckedLocking();
}
}
}
return driver;
}
}After the instance is created, subsequent calls avoid the synchronized block, improving performance.
Overall, the synchronized keyword is easy to use for simple thread‑safety scenarios, but for complex or high‑concurrency workloads developers should consider more scalable alternatives.
FunTester
10k followers, 1k articles | completely useless
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.