Search code examples
javaandroidmultithreadingsynchronize

Stopping/restarting thread in Java with notify/wait


I'm developing simple video recording app for Android 2.2+ and having trouble with getting Timer thread to work as expected. Code is below and so steps are:

  1. when user pres start recording button, recording starts and fightTimer.start() method is called.
  2. it calls timer.start() method starting to run the thread. timer is the thread object inside of fightTimer
  3. when user clicks stop button fightTimer method stop() is called. There I set a flag stopTimer=true and so that stops the thread method from running and calls timer.wait() method so the thread waits
  4. when user clicks start recording button then fightTimer.start() method is called again. It calls threads timer.start() as the timer has already started however it throws exception.
  5. I catch exception and call fightTimer.restart() method. This method set flag stopTimer I set in step 3 to true so now we have stopTimer=true. timer thread is still waiting inside of run() method
  6. then it calls timer.notify() to let timer know that it doesn't need to wait anymore and can continue running
  7. Now I was expecting the timer thread start running again but for some reason at this point execution jumps to beginning of the same method that called notify() (restart()) and sets the flag stopTimer=false, and then tries to notify timer thread again. It throws runtime exception and that's where it ends.

I assume that my understanding of whole synchronisation of threads is not correct so if anyone could point out where I screw up that would be great. The code for FightTimeris below and I don't even get any output information in logCat. Like I said any help will be much appreciated as I don't understand why is this happening and how to fix it.

    public class FightTimer extends SurfaceView implements Runnable, SurfaceHolder.Callback {

        public Thread timer;
        public SurfaceHolder surfaceHolder;
        public Canvas canvas;
        public int counter=0;
        public volatile boolean stopTimer=false;
        public volatile boolean pauseTimer=false;

        public FightTimer(Context context)
        {
            super(context);

            surfaceHolder=getHolder();

            // this is necessary to make surfaceview transparent
            surfaceHolder.setFormat(PixelFormat.TRANSLUCENT);
            setZOrderOnTop(true);

            surfaceHolder.addCallback(this);

            timer=new Thread(this);
        }


        public void start()
        {
            try
            {
                timer.start();
            }
            catch(IllegalThreadStateException ex)
            {
                reStart();
            }
        }

        public synchronized void reStart()
        {
// here the method is executed twice as I described in step 7
// after notify() it actually jumps back to stopTimer=false again and then exits the function. Then outside of this object I catch RuntimeException         
            stopTimer=false;
                    timer.notify(); 
        }

        public synchronized void pause()
        {
            pauseTimer=true;
        }

        public synchronized void resume()
        {
            pauseTimer=false;
            timer.notify(); 
        }

        public void stop()
        {
            stopTimer=true;
        }

        public void run() {

            TimeWatch timeWatch=TimeWatch.start();

            Paint paint=new Paint();

            paint.setColor(Color.RED);

            paint.setTypeface(Typeface.create("Arial",Typeface.NORMAL));
            paint.setTextSize(20);

            while(true)
            {
                // this is to pause timer

                try
                {
                    if(pauseTimer)
                    {
                        synchronized(timer) {
                            while(pauseTimer)
                                timer.wait();
                        }

                        // TODO heres the code should be executed when timer is resumed eg.
                        // maybe calculate how timer should count now as it wasn't counting for a while etc

                    }
                } catch(InterruptedException ex)
                {

                }

                // this is to pause timer

                try
                {
                    if(stopTimer)
                    {
                        synchronized(timer) {
                            while(stopTimer)
                                timer.wait();
                        }

                        // TODO heres the code should be executed when timer is restarted
                        // maybe reset timer completely etc

                        timeWatch.reset();
                    }
                } catch(InterruptedException ex)
                {

                }

                canvas=surfaceHolder.lockCanvas();

                // canvas might not exists at this point as we might be in activitis onStop() callback and stopping preview
                // etc. so we need to check if so then we exit run function

                if(canvas==null) return;

                //canvas.drawARGB(0,255,255,255);
                canvas.drawColor(0, android.graphics.PorterDuff.Mode.CLEAR); 

                long minutes=timeWatch.time(TimeUnit.SECONDS)/60;

                canvas.drawText(counter+"      "+minutes+":"+timeWatch.time(TimeUnit.SECONDS)%60,0,counter%60, paint);

                counter++;

                surfaceHolder.unlockCanvasAndPost(canvas);
            }

        }

        public void surfaceChanged(SurfaceHolder holder, int format, int width,
                int height) {
            // TODO Auto-generated method stub

            //Toast.makeText(getContext(), "Surface Changed", Toast.LENGTH_LONG).show();

        }

        public void surfaceCreated(SurfaceHolder holder) {
            // TODO Auto-generated method stub

            //timer.start();
        }

        public void surfaceDestroyed(SurfaceHolder holder) {
            // TODO Auto-generated method stub
            // when surface is destroyed it means it cannot be displayed anymore and there is no canvas to draw
            // meaning the run() method cannot draw anything and calls to surfaceHolder will throw exception
            // so we need to stop thread here
            // this will happen when activity is in onStop() callback and when is already invisible and we are going to 
            // remove the object anyway so we don't care what will happenn later and make it wait. All we need is stop
            // run() from calling any other methods on canvas from surfaceHolder 


            Toast.makeText(getContext(), "Surface Destroyed", Toast.LENGTH_LONG).show();
        }

        public void setSurfaceHolder(SurfaceHolder surfaceHolder2) {
            // TODO Auto-generated method stub
            surfaceHolder=surfaceHolder2;
        }

    }

Please also see edited comment in restart() method. Below is call stack, when restart() method exited. Please let me know if any more information is needed.

DalvikVM[localhost:8754]    
    Thread [<1> main] (Suspended)   
        <VM does not provide monitor information>   
        MyFirstAppActivity.startRecording(View) line: 271   
        Method.invokeNative(Object, Object[], Class, Class[], Class, int, boolean) line: not available [native method]  
        Method.invoke(Object, Object...) line: 521  
        View$1.onClick(View) line: 2077 
        Button(View).performClick() line: 2461  
        View$PerformClick.run() line: 8888  
        ViewRoot(Handler).handleCallback(Message) line: 587 
        ViewRoot(Handler).dispatchMessage(Message) line: 92 
        Looper.loop() line: 123 
        ActivityThread.main(String[]) line: 4627    
        Method.invokeNative(Object, Object[], Class, Class[], Class, int, boolean) line: not available [native method]  
        Method.invoke(Object, Object...) line: 521  
        ZygoteInit$MethodAndArgsCaller.run() line: 858  
        ZygoteInit.main(String[]) line: 616 
        NativeStart.main(String[]) line: not available [native method]  
    Thread [<7> Binder Thread #2] (Running) 
    Thread [<6> Binder Thread #1] (Running) 
    Thread [<8> Binder Thread #3] (Running) 
    Thread [<9> Thread-9] (Running) 

Solution

  • Methods reStart() and resume() must acquire monitor on the object timer before calling notify() on it. (Similar to how timer.wait() is being done in code above().)

    public synchronized void reStart()
    {
    // here the method is executed twice as I described in step 7
    // after notify() it actually jumps back to stopTimer=false again and then exits the function. Then outside of this object I catch RuntimeException 
        stopTimer=false;
        synchronized(timer) {
            timer.notify();
        }
    }
    
    public synchronized void resume()
    {
        pauseTimer=false;
        synchronized(timer) {
            timer.notify();
        }
    }
    

    Failure to do so results IllegalMonitorStateException on a JRE. And stacktrace posted points towards similar issue.