A common thread sync issue analysis of android application

Android应用中, 很多问题都是因为线程同步而引起, 本例涉及应用的其它方面很少, 是一个典型的线程同步问题.

A common thread sync issue.

[Symptom]
07-01 02:14:58.549 14861  2415 W dalvikvm: threadid=20: thread exiting with uncaught exception (group=0x40adc9f0)
07-01 02:14:58.619 14861  2415 E AndroidRuntime: FATAL EXCEPTION: CallLogContentHelper.UiUpdaterExecutor
07-01 02:14:58.619 14861  2415 E AndroidRuntime: java.lang.NullPointerException
07-01 02:14:58.619 14861  2415 E AndroidRuntime:  at com.xxxxx.android.socialphonebook.util.CallLogContentHelper$1.run(CallLogContentHelper.java:167)
07-01 02:14:58.619 14861  2415 E AndroidRuntime:  at com.xxxxx.android.socialphonebook.util.CallLogContentHelper$UiUpdaterExecutor$1.run(CallLogContentHelper.java:453)
07-01 02:14:58.619 14861  2415 E AndroidRuntime:  at java.lang.Thread.run(Thread.java:856)

[Root Cause]
In CallLogContentHelper.java
    private Runnable mCommandQuery;

The CallLogContentHelper use (mSyncEnable && mListener != null) as the condition to notify the listener after querySync completing.
    public CallLogContentHelper(ContentResolver contentResolver, OnQueryCompleteListener listener) {
        this(contentResolver);
        mListener = listener;
        mExecutor = new UiUpdaterExecutor();
        mCommandQuery = new Runnable() {
            public void run() {
                String selection = Calls.DATE + " > " + getCutoffDate().getTime();
                Cursor cursor = null;
                try {
                    cursor = querySync(selection, null); // do blocking query actually. time wasted.
                } catch (Exception e) {
                    SpbLog.d(TAG, "querySync(" + selection + ") failed. " + e);
                    // cursor is already null
                }
  #### synchronized(mLockListener) patches here ####
                if (cursor != null) {
166                    if (mSyncEnable && mListener != null) {
      ### point 1 ###
167                        mListener.onQueryComplete(cursor);    ********
                    } else {
                        cursor.close();
                    }
                }
            }
        };
    }


    /**
     * queryAsync Method used for asynchronous queries
     * For this method to work the OnQueryCompleteListener listener must be set
     */
    public void queryAsync() {
        synchronized (mLock) {
            if (mListener != null && mExecutor != null){
                mExecutor.execute(mCommandQuery);
            }
        }
    }

    private Cursor querySync(String selection, String[] selectionArgs) {
        SpbLog.d(TAG, "CallLogContentHelper query");
        Cursor[] cursors = new Cursor[2];
        cursors[0] = mContentResolver.query(Calls.CONTENT_URI_WITH_VOICEMAIL, CALL_LOG_PROJECTION,
                selection, selectionArgs, Calls.DEFAULT_SORT_ORDER);

        if (mVoipIsInstallled) {
            cursors[1] = mContentResolver.query(VoIPCalls.CONTENT_URI, VOIP_CALL_LOG_PROJECTION,
                    selection, selectionArgs, Calls.DEFAULT_SORT_ORDER);
        }

        return new CallLogMergeCursor(cursors);
   }


But mListener is not well synchronized in release method.
    public void release() {
        synchronized (mLock) {
            if (mExecutor != null) {
                mExecutor.tryStop();
                mExecutor = null;
            }
        }
 #### synchronized(mLockListener) patches here ####
        mListener = null;    ********
    }


UiUpdaterExecutor is an inner class of CallLogContentHelper used to update ui as its name indicates.
It executes the passed in Runnable command in itself thread using the same trick as Thread's target.run().
    static class UiUpdaterExecutor implements Executor {

        private boolean mFinish = false;

        private int mQueue;

        private Runnable mCommand;

        private Object mLock;

        public UiUpdaterExecutor() {
            mQueue = 0;
            mLock = new Object();
            Thread thread = new Thread(runner, "CallLogContentHelper.UiUpdaterExecutor");
            thread.setPriority(Thread.MIN_PRIORITY);
            thread.start();
        }

        public void execute(Runnable command) {
            SpbLog.d(TAG, "UiUpdaterExecutor execute");
            this.mCommand = command;
            synchronized (mLock) {
                mQueue = 1;
                mLock.notifyAll();
            }
        }

        public void tryStop() {
            mFinish = true;
            synchronized (mLock) {
                mLock.notifyAll();
            }
        }

        private Runnable runner = new Runnable() {
            public void run() {
                while (!mFinish) {
                    boolean doExecute = false;
                    synchronized (mLock) { **** UiUpdaterExecutor owner lock. ****
                        if (mQueue <= 0) {
                            try {
                                mLock.wait();
                            } catch (InterruptedException e) {
                                // Do nothing
                            }
                        } else {
                            mQueue--;
                            doExecute = true;
                        }
                    }
                    if (doExecute) {
                        SpbLog.d(TAG, "UiUpdaterExecutor commnad run");
                        mCommand.run(); **********
                    }
                }
                SpbLog.d(TAG, "UiUpdaterExecutor finished");
            }
        };
    }

So, after CallLogContentHelper object is constructed, the execute thread is running and waiting for commnad.
When CallLogContentHelper::queryAsync is called in main thread and returns immediately, command is delivered to executor and command.run() method is executed in executor thread.
The database query is relatively a time wasted process.

Say the executor thread evaluates mListener != null and is scheduled out at ### point 1 ###.
Now, the main thread or another thread calls CallLogContentHelper::release() and assigns mListener with null.
When the executor thread continues to run, mListener is null and the java.lang.NullPointerException arise.
 
More accrutely, the executor thread is absolutely immune from the CallLogContentHelper object's lock.

The CallLogContentHelper::release() is usaully called in activity's or service's onDestroy(). 
In normal use, the crash may not happens.
But the crash possibility much increase in automation test, because the activity may be created and destroyed very quickly.
 
The reference solution is to use a synchronized lock to protect the mListener.
synchronized(mLockListener) {}.

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值