WebViewer
WebViewer

Reputation: 831

How does Object.notify() work with Object.wait()?

I am trying to track a resource which behaves in an asymmetric manner. That is, it responds immediately to a start() request, but finishes processing a cancel() request at a significant delay.

For this purpose, I created an AtomicBoolean flag, with the intent to set it (true) immediately, but clear it (false) via a CountDownTimer.

protected AtomicBoolean mIsAsymmetricMode = new AtomicBoolean(false);

This flag is never set directly by any of the classes involved. It is always set and cleared via a dedicated method.

All callers of this setAsymmetricMode(boolean) method, run on the same thread - the main (UI) thread.

On a different thread I have an operation waiting for this flag to be cleared (false).

The following is a minimal working MainActivity.java that demonstrates the issue:

package com.example.basicactivity;

import static com.example.basicactivity.Log.logE;
import static com.example.basicactivity.Log.logW;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import android.os.CountDownTimer;
import android.view.View;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import androidx.navigation.ui.AppBarConfiguration;
import androidx.navigation.ui.NavigationUI;
import com.example.basicactivity.databinding.ActivityMainBinding;
import android.view.Menu;
import android.view.MenuItem;
import java.util.concurrent.atomic.AtomicBoolean;

public class MainActivity extends AppCompatActivity {
    private AppBarConfiguration appBarConfiguration;
    private ActivityMainBinding binding;
    protected final AtomicBoolean mIsAsymmetricMode = new AtomicBoolean(false);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        setSupportActionBar(binding.toolbar);

        NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main);
        appBarConfiguration = new AppBarConfiguration.Builder(navController.getGraph()).build();
        NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);

        /*
          logE() waiting for mIsAsymmetricMode set to FALSE
         */
        binding.fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                logW("ASYMMETRIC", "^");
                new Thread( () -> {
                    synchronized(mIsAsymmetricMode) {
                        while (mIsAsymmetricMode.get()) {
                            try {
                                mIsAsymmetricMode.wait();
                            } catch (InterruptedException e) {
                                Thread.currentThread().interrupt();
                            }
                        }
                        logE("ASYMMETRIC", "mIsAsymmetricMode=false");
                    }
                }).start();
                logW("ASYMMETRIC", "$");
            }
        });
    }

    protected void setAsymmetricMode(boolean is) {
        logW("ASYMMETRIC", "^ " + is);

        if (is) {
            mIsAsymmetricMode.compareAndSet(false, true);
        }
        else {
            new CountDownTimer(100, 50) {
                int tickCounter = 0;
                public void onTick(long millUntilFinish) {
                    tickCounter++;
                    logW("ASYMMETRIC", "tickCounter=" + tickCounter);
                }

                public void onFinish() {
                    logW("ASYMMETRIC", "^");
                    synchronized(mIsAsymmetricMode) {
                        mIsAsymmetricMode.compareAndSet(true, false);
                        mIsAsymmetricMode.notifyAll();
                    }
                    logW("ASYMMETRIC", "$");
                }
            }.start();
        }
        logW("ASYMMETRIC", "$");
    }

    @Override // TRUE test (immediate)
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        setAsymmetricMode(true);
        return true;
    }

    @Override // FALSE test (delayed)
    public boolean onOptionsItemSelected(MenuItem item) {
        if (item.getItemId() == R.id.action_settings) {
            setAsymmetricMode(false);
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    @Override
    public boolean onSupportNavigateUp() {
        NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main);
        return NavigationUI.navigateUp(navController, appBarConfiguration) || super.onSupportNavigateUp();
    }
}

When run, it presents a screen with two areas triggering listeners in this code:

basicactivity_annoated

The code works as expected, that is, when the "envelope button" is touched first, no logE() message is output to Logcat (it is waiting on the mIsAsymmetricMode flag), then when the "settings button" is touched, the logE() message is printed (last one, starting with "16:52:00.607 E T=8382"):

---------------------------- PROCESS STARTED (8121) for package com.example.basicactivity ----------------------------
16:51:22.206   W  T=8121 MainActivity.setAsymmetricMode < MainActivity.onCreateOptionsMenu | ^ true
16:51:22.207   W  T=8121 MainActivity.setAsymmetricMode < MainActivity.onCreateOptionsMenu | $
16:51:34.163   W  T=8121 MainActivity$1.onClick < View.performClick | ^
16:51:34.165   W  T=8121 MainActivity$1.onClick < View.performClick | $
16:52:00.504   W  T=8121 MainActivity.setAsymmetricMode < MainActivity.onOptionsItemSelected | ^ false
16:52:00.508   W  T=8121 MainActivity.setAsymmetricMode < MainActivity.onOptionsItemSelected | $
16:52:00.518   W  T=8121 MainActivity$2.onTick < CountDownTimer$1.handleMessage | tickCounter=1
16:52:00.568   W  T=8121 MainActivity$2.onTick < CountDownTimer$1.handleMessage | tickCounter=2
16:52:00.606   W  T=8121 MainActivity$2.onFinish < CountDownTimer$1.handleMessage | ^
16:52:00.607   W  T=8121 MainActivity$2.onFinish < CountDownTimer$1.handleMessage | $
16:52:00.607   E  T=8382 MainActivity$1.lambda$onClick$0$com-example-basicactivity-MainActivity$1 < MainActivity$1$$ExternalSyntheticLambda0.run | mIsAsymmetricMode=false

Now... what I am trying to understand is why when I remove the

mIsAsymmetricMode.notifyAll();

statement in onFinish(), that logE() message is never released as it seems that the while loop gets stuck at the

mIsAsymmetricMode.wait();

and never gets to check for the flag and see that it changed to false.

In other words, are there any circumstances in which the while (mIsAsymmetricMode.get()) loop will terminate without issuing the mIsAsymmetricMode.notifyAll() (or mIsAsymmetricMode.notify()) call?


Update: After being helped by the accepted answer, I highly recommend testing the flag before entering the while() loop - despite it being tested by the while() loop anyway. The reason is that it allows placing debug logs as follows:

            new Thread( () -> {
                synchronized(mIsAsymmetricMode) {
                    if (mIsAsymmetricMode.get()) {
                        logD("ASYMMETRIC", "while() loop entered");
                        while (mIsAsymmetricMode.get()) {
                            try {
                                mIsAsymmetricMode.wait();
                            } catch (InterruptedException e) {
                                Thread.currentThread().interrupt();
                            }
                        }
                    }
                    else {
                        logW("ASYMMETRIC", "while() loop NOT entered");
                    }
                }
            }).start();

Upvotes: 1

Views: 90

Answers (1)

Jay Souper
Jay Souper

Reputation: 2646

Here is how it works in general:

  1. Thread acquires the lock on the object (mIsAsymmetricMode in your case).
  2. Thread releases the lock on the object and enters the waiting state.
  3. Other thread acquires the lock on the object and make changes to the object.
  4. Other thread releases the lock on the object.
  5. The thread that was waiting is notified via notify() or notifyAll().
  6. The thread wakes up and re-acquires the lock on the object.
  7. The thread checks the object state and exits the loop if the condition is met.

Notice #5 and #6: Without notify() or notifyAll(), there is no chance for the waiting thread of ever knowing that the object state has changed. Therefore it has no chance of exiting the loop via met condition.

This confirms what you are seeing on the code you posted. It works correctly.

If you have more complex code that exhibits a different behavior, a possible explanations could be a race condition, where the value of mIsAsymmetricMode is being changed to false right before the thread enters the wait state.

Also, check whether the AtomicBoolean in your other code is final.

Upvotes: 1

Related Questions