5

I was reading on multithreading in Java and using synchronized blocks. Suppose I have two different, independent synchronized blocks. I can have them run in parallel, by using a lock each for both the synchronized blocks. But if I use the same lock for both synchronized blocks, I think only one can run at a given time. Am i wrong to think so? If no, why am I getting the below strange result?

Assume I have two independent operations, increment1 and increment2, called by a different thread each.

public class AppMultipleSynchronization {

    private static int counter1 = 0;
    private static int counter2 = 0;

    private static Object lock1 = new Object();
    private static Object lock2 = new Object();

    public static void increment1() {
        synchronized (lock1) {
            counter1++;
        }
    }

    public static void increment2() {
        synchronized (lock2) {
            counter2++;
        }
    }

    public static void main(String[] args) {

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000000; i++) {
                    increment1();
                }
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000000; i++) {
                    increment2();
                }
            }
        });

        long startTime = System.currentTimeMillis();

        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        long endTime = System.currentTimeMillis();

        System.out.println("total time taken: " + (endTime - startTime) + "ms");
        System.out.println("counter1: " + counter1 + "\ncounter2: " + counter2);
    }
}

As you can see, I am using a different lock for for both increments. To use the same lock, use the exact same program, replace with lock1 in both cases.

Output in case of two locks:

    total time taken: 13437ms
    counter1: 100000000
    counter2: 100000000

Output in case of single lock:

    total time taken: 5139ms
    counter1: 100000000
    counter2: 100000000
Gray
  • 115,027
  • 24
  • 293
  • 354
keemahs
  • 780
  • 11
  • 14
  • 1
    How many processors do you have? Try to find out the time taken instead of synchronized(AppMultipleSynchronization.class) use synchronized(lock1) – Gunwant Jul 14 '21 at 07:20
  • 3
    Using JMH, assuming I set everything up correctly, I don't see such a dramatic difference in average execution time: One lock, `487.514 ± 22.413 ms/op`; Two locks, `1064.114 ± 24.043 ms/op`. That's only just over a half second longer to execute. Note I looped ten million times instead of 100 million times. – Slaw Jul 14 '21 at 09:01
  • @Gunwant i had tried lock1 instead of AppMultipleSynchronization.class..but still same result... – keemahs Jul 14 '21 at 14:04
  • 1
    There are a bit too many factors to figure out experimentally... First, you're synchronizing on non-final fields. Even though descriptions on what final does for synchronized block only mention safety and you don't change the value, adding final cuts the time in half in my tests. Secondly, some big optimization must be kicking in - if I put the code of main method in a loop the times taken by different iterations are wildly inconsistent, sometimes 10 times longer. And thirdly, for me locally the approach with two locks is faster than both on lock1. – Deltharis Jul 14 '21 at 14:55
  • Also : how many loops is `t1` able to do before `t2` even starts? – bowmore Jul 14 '21 at 23:15
  • 1
    Almost every time a question like "the performance of this Java thing doesn't behave how I expect it to" comes up the correct answer is [your benchmark is wrong](https://stackoverflow.com/questions/504103/how-do-i-write-a-correct-micro-benchmark-in-java). Writing correct micro-benchmarks in Java is hard and if you don't at least use an existing tool/library for this, then the odds are extremely high that you're not considering all the possible pitfalls. – Joachim Sauer Jul 16 '21 at 16:44

1 Answers1

4

am i wrong to think so? if no, why am i getting the below strange result?

No you are not wrong. If you are using 2 locks then 2 threads can be executing the 2 different blocks at the same time.

What you are seeing is most likely because these sorts of fine grained, threaded, performance tests are very subtle. This may have much more to do about branch and lock prediction than about true runtime. The JVM makes guesses about whether a lock should be tested in a spin loop versus a true mutex wait/queue. It can dramatically optimize this code given that the actual work is just ++. It may also be that t1.start() starts and runs so fast that it finishes before the t2.start() can be executed.

If you change the lock body to be more substantial, you should start to see that the 2 locks will result in faster wall clock runtime. For example, if you do a loop of ++ operations in the block:

public static void increment1() {
    synchronized (lock1) {
        for (int i = 0; i < 100000000; i++) {
            counter1++;
        }
    }
}
...
public void run() {
    for (int i = 0; i < 10000; i++) {
        increment1();
    }
}

Then with 1 locks I get:

total time taken: 62822ms
counter1: -727379968
counter2: -727379968

but 2 locks gets:

total time taken: 30902ms
counter1: -727379968
counter2: -727379968

Lastly, the JVM does a whole bunch of class loading, gcc -O3 equivalence, and machine code inlining during the first seconds and even minutes of runtime. Anytime you are trying to run Java performance tests, you need to make sure your program is running for a long time to get some level of accurate numbers.

Gray
  • 115,027
  • 24
  • 293
  • 354
  • The inner loop can be completely optimized-out by the JIT. It can be replace e.g. "synchronized(..){counter+=100000000;}" – pveentjer Jul 15 '21 at 11:57
  • Could be, yes but it seems to prove the point in this case. Did you try it @pveentjer? – Gray Jul 16 '21 at 16:40