Call a synchronized method with a new thread in a synchronized method

86 views Asked by At

We have a task to perform, dummyMethod.

private synchronized void dummyMethod(){
    Log.d("debug", "do nothing in dummyMethod()")
}

We run that task several times.

private synchronized void fooMethod() throws InterruptedException{
    
    for (int i = 0; i < 10; i++){
        dummyMethod();
    }
   
   Thread.sleep(10000)
   Log.d("debug", "fooMethod() finished")
}

With the above code synchronized dummyMethod() is executed 10 times immediately.

However, with the following code, dummyMethod() is called only once immediately, and only after fooMethod() is finished did it get finished and executed 9 more times.

private synchronized void fooMethod() throws InterruptedException{
    
 new Thread(()->{
    for (int i = 0; i < 10; i++){
        dummyMethod();
    }
 }).start();
   
   Thread.sleep(10000)
   Log.d("debug", "fooMethod() finished")
}

In this case, logcat shows:

Long monitor contention with owner main (18077) at void ...fooMethod()(MainActivity.java:1106) waiters=0 in void ...MainActivity.dummyMethod() for 10.001s

I thought it would not block with a new thread in the latter case. Could anyone shed some light on this? How can a synchronized method run another synchronized method multiple times asynchronously in a new thread?

2

There are 2 answers

1
Andrew S On BEST ANSWER

From my comment -

From this Baeldung article: all synchronized blocks of the same object can have only one thread executing them at the same time

So the current thread is already in a synchronized method, and the new thread cannot immediately execute dummyMethod() since it's the same object.

Slight mod to the original code, showing use of a new instance does allow the new Thread to immediately run dummyMethod():

package sandbox;

public class SyncTest {

    public static void main(String[] args) throws InterruptedException {
        var test = new SyncTest();
        test.fooMethod();
    }

    private synchronized void dummyMethod() {
        System.out.println("do nothing in dummyMethod()");
    }

    private synchronized void fooMethod() throws InterruptedException {
        var t = new SyncTest(); // new instance will have its own monitor
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                t.dummyMethod();
            }
        }).start();

        Thread.sleep(10000);
        System.out.println("fooMethod() finished");
    }
}
1
Basil Bourque On

[Caveat: I am not an expert on Java concurrency.]

I made a complete self-contained version of your code.

This code uses CopyOnWriteArrayList as a thread-safe log.

package work.basil.example.threading;

import java.time.Duration;
import java.time.Instant;
import java.util.SequencedCollection;
import java.util.concurrent.CopyOnWriteArrayList;

public class App
{
    private SequencedCollection < String > log = new CopyOnWriteArrayList <> ( );  // Thread-safe `List` implementation.

    public static void main ( String[] args )
    {
        App app = new App ( );
        app.demo ( );
    }

    private void demo ( )
    {
        this.log.add ( "info | demo start | " + Instant.now ( ) );
//        this.fooMethod ( );
        this.fooMethodThreaded ();
        this.log.add ( "info | demo end | " + Instant.now ( ) );
        log.forEach ( System.out :: println );
    }

    private synchronized void dummyMethod ( )
    {
        this.log.add ( "debug | do nothing in dummyMethod() | " + Instant.now ( ) );
    }

    private synchronized void fooMethod ( )
    {
        for ( int i = 0 ; i < 10 ; i++ ) dummyMethod ( );
        try { Thread.sleep ( Duration.ofSeconds ( 10 ) ); } catch ( InterruptedException e ) { throw new RuntimeException ( e ); }
        this.log.add ( "debug | fooMethod() finished | " + Instant.now ( ) );
    }

    private synchronized void fooMethodThreaded ( )
    {
        new Thread ( ( ) ->
        {
            for ( int i = 0 ; i < 10 ; i++ ) dummyMethod ( );
        } ).start ( );
        try { Thread.sleep ( Duration.ofSeconds ( 10 ) ); } catch ( InterruptedException e ) { throw new RuntimeException ( e ); }
        this.log.add ( "debug | fooMethodThreaded() finished | " + Instant.now ( ) );
    }
}

When run:

info | demo start | 2023-10-19T19:42:38.662271Z
debug | fooMethodThreaded() finished | 2023-10-19T19:42:48.708631Z
info | demo end | 2023-10-19T19:42:48.709687Z
debug | do nothing in dummyMethod() | 2023-10-19T19:42:48.714712Z
debug | do nothing in dummyMethod() | 2023-10-19T19:42:48.715071Z
debug | do nothing in dummyMethod() | 2023-10-19T19:42:48.715137Z
debug | do nothing in dummyMethod() | 2023-10-19T19:42:48.715205Z
debug | do nothing in dummyMethod() | 2023-10-19T19:42:48.715264Z
debug | do nothing in dummyMethod() | 2023-10-19T19:42:48.715321Z

Examine the timestamps. Notice that after the demo starts nothing gets logged for a full ten seconds. Why the wait? We have to look at a few facts.

Be aware that our two methods, dummyMethod & fooMethodThreaded, are both instance methods on the same class, App, and both are declared as synchronized. Synchronized methods on the same class will synchronize on the whole instance, not on each method separately. So, synchronized methods on the same object will block each other; only one can run at a time. See Java synchronized method lock on object, or method?.

Add the fact noted in the Comment by Andrew S:

all synchronized blocks of the same object can have only one thread executing them at the same time

You have an instance of App running in the main thread. There we run the fooMethodThreaded method. That synchronized method gets a lock on our one and only App instance. Then we spawn a thread to run the other method dummyMethod on the same single instance of App.

At this point, we now we have a lock conflict. The fooMethodThreaded already holds the lock on our instance of App named app. So when the other method dummyMethod, being synchronized, tries to obtain the same lock on the same app object, it finds the lock already taken. So the dummyMethod blocks, waiting for that lock to release.

And so we sit waiting. Meanwhile, the original thread that holds the lock on the singular App instance named app is sleeping. That main thread sleeps for ten seconds. At the end of that ten seconds, the fooMethodThreaded method of our app instance exits. The lock on app then releases, as the synchronized method exited.

With the original lock released, then we see a cascade of log entries as the background thread performs its work, with successive calls to dummyMethod able to access the lock on app each time.

Notice that we do not see ten entries in the log for "do nothing in dummyMethod()". That is because our main method has moved on to its log.forEach before the background thread has completed.

Solution

If you truly wanted to perform this task ten times on background thread(s), I would suggest something like the code seen below.

If you want to limit the running of our ten tasks to only ten at a time, use something other than synchronized on the method. We have learned that having two methods on the same class using synchronized will block each other. That dual-method-blocking is not our goal. So use another approach to locking. One approach is to synchronize on a separate object within the method rather than synchronize on the method. See this Answer on the Question, Other way to synchronize method. I would prefer using an explicit approach as shown in this Answer on that same Question. See also another Question, can synchronized at method level be replaced by Lock?.

In modern Java, we rarely address Thread directly. Instead, use the Executors framework added to Java 5.

Take advantage of the fact that ExecutorService recently became AutoCloseable. So we can use it in try-with-resources syntax.

If your task is not CPU-bound, that is, if it does some blocking, use virtual threads.

private void fooMethodWithExecutorService ( )
{
    this.onlyTenTasksAtATimeLock.lock ( );
    try
    {
        try (
                ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor ( ) ;
        )
        {
            int countTaskExecutions = 10;
            for ( int index = 0 ; index < countTaskExecutions ; index++ )
            {
                executorService.submit ( this :: dummyMethod );
            }
        }
    }
    finally
    {
        this.onlyTenTasksAtATimeLock.unlock ( );
    }
}

With this approach, we see the demo start and end at the very start and the very end. And we see each and every desired execution of our task make its entry in the log.

info | demo start | 2023-10-19T21:19:42.158048Z
debug | do nothing in dummyMethod() | 2023-10-19T21:19:42.165986Z
debug | do nothing in dummyMethod() | 2023-10-19T21:19:42.166079Z
debug | do nothing in dummyMethod() | 2023-10-19T21:19:42.166119Z
debug | do nothing in dummyMethod() | 2023-10-19T21:19:42.166210Z
debug | do nothing in dummyMethod() | 2023-10-19T21:19:42.166234Z
debug | do nothing in dummyMethod() | 2023-10-19T21:19:42.166268Z
debug | do nothing in dummyMethod() | 2023-10-19T21:19:42.166294Z
debug | do nothing in dummyMethod() | 2023-10-19T21:19:42.166318Z
debug | do nothing in dummyMethod() | 2023-10-19T21:19:42.166334Z
debug | do nothing in dummyMethod() | 2023-10-19T21:19:42.166370Z
info | demo end | 2023-10-19T21:19:42.166437Z

Complete app code:

package work.basil.example.threading;

import java.time.Duration;
import java.time.Instant;
import java.util.SequencedCollection;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class App
{
    private final SequencedCollection < String > log = new CopyOnWriteArrayList <> ( );  // Thread-safe `List` implementation.
    private final Lock onlyTenTasksAtATimeLock = new ReentrantLock ( );

    public static void main ( String[] args )
    {
        App app = new App ( );
        app.demo ( );
    }

    private void demo ( )
    {
        this.log.add ( "info | demo start | " + Instant.now ( ) );
//        this.fooMethod ( );
//        this.fooMethodThreaded ();
        this.fooMethodWithExecutorService ( );
        this.log.add ( "info | demo end | " + Instant.now ( ) );
        log.forEach ( System.out :: println );
    }

    private void fooMethodWithExecutorService ( )
    {
        this.onlyTenTasksAtATimeLock.lock ( );
        try
        {
            try (
                    ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor ( ) ;
            )
            {
                int countTaskExecutions = 10;
                for ( int index = 0 ; index < countTaskExecutions ; index++ )
                {
                    executorService.submit ( this :: dummyMethod );
                }
            }
        }
        finally
        {
            this.onlyTenTasksAtATimeLock.unlock ( );
        }
    }

    private synchronized void dummyMethod ( )
    {
        this.log.add ( "debug | do nothing in dummyMethod() | " + Instant.now ( ) );
    }

    private synchronized void fooMethod ( )
    {
        for ( int i = 0 ; i < 10 ; i++ ) dummyMethod ( );
        try { Thread.sleep ( Duration.ofSeconds ( 10 ) ); } catch ( InterruptedException e ) { throw new RuntimeException ( e ); }
        this.log.add ( "debug | fooMethod() finished | " + Instant.now ( ) );
    }

    private synchronized void fooMethodThreaded ( )
    {
        new Thread ( ( ) ->
        {
            for ( int i = 0 ; i < 10 ; i++ ) dummyMethod ( );
        } ).start ( );
        try { Thread.sleep ( Duration.ofSeconds ( 10 ) ); } catch ( InterruptedException e ) { throw new RuntimeException ( e ); }
        this.log.add ( "debug | fooMethodThreaded() finished | " + Instant.now ( ) );
    }
}