Using Strings as Synchronization Locks in Java

Using Strings as Synchronization Locks in Java

In Java, the String class possesses unique characteristics due to its implementation of the string constant pool. Although the implementation details changed in JDK 1.8 and later versions, the fundamental behavior remains consistent.

This distinctive feature allows us to utilize String objects as synchronization locks. For instance, when updating user information, we can employ the user's name as a lock, ensuring that different users utilize distinct locks and thereby enhancing concurrent performance. This approach can be extended to numerous scenarios with appropriate implementation.

However, due to the special nature of Strings, Java provides additional string-related utility classes such as StringBuffer and StringBuilder. Moreover, while strings represent constant values, they can also be instantiated using new() to create mutable objects. These nuances can significantly impact thread synchronization behavior.

Let's examine these scenarios through practical examples.

Testing with new String() Instances

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class StringLockTest {
    private static Integer COUNTER = 0;
    
    public static void main(String[] args) {
        final String lockObject = new String(UUID.randomUUID().toString());
        executeTest(lockObject);
    }

    private static void executeTest(String lockObject) {
        final Integer threadCount = 10;
        final CyclicBarrier barrier = new CyclicBarrier(threadCount, new Runnable() {
            public void run() {
                System.out.println("Thread count: " + threadCount);
            }
        });
        
        for(int i = 0; i < threadCount; i++) {
            String tempLock = new String(lockObject);
            new WorkerThread(barrier, tempLock.toString()).start();
        }
    }

    static class WorkerThread extends Thread {
        private CyclicBarrier barrier;
        private String lock;
        
        public WorkerThread(CyclicBarrier barrier, String lock) {
            this.barrier = barrier;
            this.lock = lock;
        }
        
        public void run() {
            try {
                barrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
            
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            synchronized(lock) { // Using String object as lock
                COUNTER = COUNTER + 1;
                System.out.println("Value: " + COUNTER);
            }            
        }
    }
}

The output shows:

Thread count: 10
Value:2
Value:2
Value:2
Value:2
Value:4
Value:5
Value:5
Value:4
Value:4
Value:4

The results indicate that each thread creates a different lock when using new String(), causing synchronization to fail. This occurs because new String(lock) generates distinct objects, each pointing to different object locks.

Isssues with StringBuilder and StringBuffer

Extending from the previous example, we encounter similar challenges with StringBuilder and StringBuffer when used as synchronization locks. For instance, in scenarios requiring string concatenation for locks, such as combining username and organization name:

StringBuilder tempLock = new StringBuilder();
tempLock.append("username");
tempLock.append("organization");
for(int i = 0; i < threadCount; i++) {
    new WorkerThread(barrier, tempLock.toString()).start();
}

The resultnig output:

Thread count: 10
Value:2
Value:2
Value:2
Value:3
Value:2
Value:3
Value:2
Value:3
Value:2
Value:2

This approach also fails as a synchronizasion mechanism. The problem lies in the StringBuilder's toString() method, which returns a new String instance:

@Override
public String toString() {
    // Create a copy, don't share the array
    return new String(value, 0, count);
}

Consequently, each thread receives a different string object, preventing effective synchronization.

Solutions

From the examples above, we can see that using String as a synchronization lock requires careful attention to ensure all threads reference the same String object. The simplest approach is to use a single String instance, but this can be challenging in distributed environments.

For example, if usernames are stored in Redis, threads fetching data during synchronization might create new String objects. In such cases, we can use the intern() method to ensure string interning. Modifying the previous code:

synchronized(lock.intern()) {
    COUNTER = COUNTER + 1;
    System.out.println("Value: " + COUNTER);
}

This approach directly references the string's value rather than the String object itself, ensuring that identical strings reference the same object and consequently the same lock within the same process.

Test Results

Thread count: 10
Value:1
Value:2
Value:3
Value:4
Value:5
Value:6
Value:7
Value:8
Value:9
Value:10

Tags: java Synchronization string Concurrency thread-safety

Posted on Thu, 18 Jun 2026 18:00:08 +0000 by Leppy