The labs on this site need JavaScript to run. The same content is written out below.
Fundamentals › Java
What is a lock, and what happens without one?
A lock is a mark that says "only one thread runs this section at a time." In this code, the part that sets the lock is the synchronized before the declaration of the add method. What add does is the single line count++, and this one line is three steps: read the value of count, add 1, and write the added value to count. If two threads change the same count at the same time, another thread can change count after one thread has read the value and before it writes, and one thread's increment can be lost. When synchronized is attached, a thread must acquire the lock before entering add, and only one thread can hold the lock at a time, so the other thread waits until the previous thread leaves add. In this lab you add and remove synchronized and switch between 2 threads and 1 thread. The same code is compiled and really run, and you see the count printed at the end. (Creating threads and handling them with start and join is covered in the lab "Threads". static is read as meaning that count and add exist only once in the program, and it is not covered in detail here.)
Checked against real execution · Compiled 4 combinations with javax.tools on JDK 21, ran each 30 times as a child java process, and checked the final count (with 2 threads and no lock, all 30 runs were below 200000)
public class Main {
// A variable that two threads change together. There is only one in the program
static int count = 0;
// Lock: with synchronized, only one thread runs at a time
static synchronized void add() {
// count++ is three steps: read, add, write
count++;
}
public static void main(String[] args) throws Exception {
// The work of a thread: call add() 100000 times
Runnable job = () -> {
for (int i = 0; i < 100000; i++) {
add();
}
};
Thread t1 = new Thread(job);
// The second thread: does the same work
Thread t2 = new Thread(job);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
The compiler and runtime are JDK 21.0.12.1 (javac 21). All 4 combinations were checked against real runs: the same source as the code on the screen (identical line by line) was compiled with javax.tools, and the java of this JDK was started as a child process and actually run 30 times for each combination, and the final count that was printed was collected and compared. In all 120 runs the exit code was 0 and the standard error was empty. For the two combinations with synchronized, the output was 200000 in all 30 runs with 2 threads and 100000 in all 30 runs with 1 thread. The combination without synchronized and with 1 thread was also 100000 in all 30 runs. The combination without synchronized and with 2 threads was smaller than 200000 in all 30 runs, and there were 30 different values (between 119684 and 155244). So the result cell for this combination is not a fixed number but "A value smaller than 200000 (different on each run)". This is what was seen on the PC used to write this lab (12 logical processors), and how many increments are lost can differ between computers and between runs, so the screen does not state the amount lost as a fixed fact. The numbers 5, 6, and 7 in the scenes and the order in which t1 and t2 step in are one example chosen to show the principle, and that order was not observed in a real run. In the real runs, many more increments were lost than in this example's single one (44756 to 80316 short of the expected value), and I did not break down separately why that many are lost. What this lab checked is the fact that increments are lost and the fact that the operation is split into three steps, and it did not check how those two facts produce the amount that is lost. The facts that the step sentences rely on were checked with separately written source. First, when I looked at count++ inside add() of the same source with javap, the order was getstatic (read), iconst_1 and iadd (add 1), and putstatic (write). Second, while t1 held the lock inside a static synchronized method and t2 called the same method, the state of t2 was BLOCKED, Thread.holdsLock(Main.class) showed true, which shows that the lock belongs to the Class object of Main, and after both finished, count was 2 (the same in all 30 runs). For the documentation, I opened the Java Language Specification (Java SE 21) myself. That only one thread at a time can hold the lock of a monitor and other threads stop until they can hold it, and that a static method uses the monitor of the Class object, follow section 17.1; that a synchronized method acquires the monitor before it executes, and that a static method uses the monitor of the Class object and an instance method uses the monitor of this, follow section 8.4.3.6; and that the lock is released automatically when the method ends follows the explanation at the beginning of chapter 8. Section 8.4.3.6 itself did not state when the lock is released. That a lock release by one thread is ordered before a later acquisition of the same lock by another thread is the content of section 17.4.5, and this lab did not cover the problem of whether values become visible. That count++ is divided into three steps of getting the value, adding 1, and storing it back, and that when the steps of two threads mix, an update can be lost, I confirmed by opening Thread Interference and Synchronized Methods in the Synchronization part of the Java Tutorials. However, no document specifies how many times it is lost in this code, and that is the value from running it on this JDK. I read the documents through web page summaries and did not read the whole original text myself. The code on the screen uses only classes inside java.lang, so it has no import line. Beyond that, synchronized blocks, the lock that each object has separately (instance methods), volatile and AtomicInteger, the order in which locks are released and fairness, and deadlock, in which threads wait for each other and stop, are not covered. Why increments are lost and a comparison of locks are covered in the in-depth lab "Race condition", and deadlock in "Deadlock".
Previous lab: Threads · Next lab: Thread pools
Struck-through text was removed from the first version of the code; highlighted text was added.