【问题标题】:Android Thread Blocking UI Unless Using SynchronizedAndroid 线程阻塞 UI 除非使用同步
【发布时间】:2015-06-12 03:03:45
【问题描述】:

我试图找出我手机的相机可以拍照的最快速度。我用来管理相机的对象(没有相机逻辑)如下所示:

public class PictureTaker {
    private boolean mIsTakingPicture;

    public void takePicture() {
        mIsTakingPicture = true;
    }

    public boolean isTakingPicture() {
        return mIsTakingPicture;
    }
} 

在我的活动中,我创建了一个新线程并向其发布一个可运行的线程,以便在用户点击按钮时开始循环和拍照。我再次点击按钮停止线程。

import android.os.Handler;
import android.os.HandlerThread;
import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

public class CameraTestActivity extends ActionBarActivity {

    private HandlerThread mThread;
    private Handler mThreadHandler;
    private volatile boolean mTestingCameraSpeed;
    private PictureTaker mPictureTaker;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_camera_test);

        mThread = new HandlerThread("testThread");
        mThread.start();
        mThreadHandler = new Handler(mThread.getLooper());
        mPictureTaker = new PictureTaker();
    }

    public void toggleDoingStuff(final View view) {
        Button b = (Button)view;
        if(mTestingCameraSpeed) {
            mTestingCameraSpeed = false;
            b.setText("Start camera");
        } else {
            mTestingCameraSpeed = true;

            mThreadHandler.post(new Runnable() {
                public void run() {
                    loopUntilStop();
                }
            });
            b.setText("Stop camera");
        }
    }

    private void loopUntilStop() {
        for(;;) {
            if(!mTestingCameraSpeed) break;

            if(!mPictureTaker.isTakingPicture()) {
                Log.d("CameraTestActivity", "Taking picture at time " + System.currentTimeMillis());
                mPictureTaker.takePicture();
            }
        }
    }
} 

由于布尔值是原子的,我不介意错过更新,因此我没有将 loopUntilStop() 中的 mIsTestingCameraSpeed 检查包装在 synchronized 块中。 (编辑:正如 mttdbrd 和 Kevin Krumwiede 指出的那样,如果没有同步或使变量易变,线程可能看不到更改)。

无论如何,上面的代码阻塞了 UI 线程。但是,将takePicture() 更改为

public void takePicture() {
    mIsTakingPicture = false;
}

不会阻止 UI,但是 mIsTakingPicture 设置不正确。为了防止阻塞并使用takePicture()的第一个版本,我不得不将isTakingPicture()更改为使用锁:

public boolean isTakingPicture() {
    synchronized (this) {
        return mIsTakingPicture;
    }
}

当mIsTakingPicture 设置为true 而不是false 时,为什么不使用锁阻止UI?我检查了Java documentation on intrinsic locks and synchronization 并没有找到任何可以解释的东西。我也看过这些 SO 问题,但没有一个回答我的问题:

编辑: Here 是 ANR 的日志。前两行表明线程挂起等待锁,从第 216 行看,似乎测试线程确实在某个时间点获得了锁。从 thread_list.cc 的第 161 行开始,操作系统尝试获取两个不同的互斥锁,但我不确定它们是否与问题有关。

【问题讨论】:

    标签: java android multithreading concurrency


    【解决方案1】:

    首先,确保您甚至可以在 UI 线程之外使用相机 API。除非明确记录您可以,否则您应该假设您不能。 Android 的 API 大多设计为在 UI 线程中调用。

    其次,Java 中的线程安全不仅仅是原子性;您还必须担心能见度。工作线程可能看不到对mTestingCameraSpeed 的写入,除非您将其设为volatile 或同步读取和写入。同样,同步读取mIsTakingPicture 是没有意义的,除非您也同步​​写入。 (或者再次,设为volatile。)

    第三,您所描述的问题没有任何意义,因此它实际上可能是由上述问题之一引起的。

    假设您使用的是Camera#takePicture(...),我建议您使用其中一个回调来开始拍摄下一张照片。这应该会给你最大的实用率。

    编辑:mThread.getLooper() 在调用mThread.start() 后应该不可能返回null,而且我不确定如果这样做会发生什么。所以这只是在黑暗中的一个镜头。但是尝试改变这个:

        mThread.start();
        mThreadHandler = new Handler(mThread.getLooper());
    

    ...到这个:

        mThread.start();
        Looper looper = mThread.getLooper();
        Log.d("DERP", "looper is " + looper);
        mThreadHandler = new Handler(looper);
    

    【讨论】:

    • 感谢您为 UI 线程设计的大多数 API 的提醒;我确实在主线程之外拍照,但我想我不应该依赖它。这些都是关于并发的有效点,但我不明白为什么 UI 线程会在上述情况下阻塞或死锁;它在哪里等着呢?
    • @Shane_S 你提到你已经为这篇文章简化了你的代码。会不会是工作线程在同步块内循环,因此持有锁?
    • 上面写的代码仍然阻塞(即isTakingPicture()中没有synchronized)。
    • @Shane_S 恐怕我没有主意了。
    • 我已经添加了一个指向 ANR 日志的链接,如果这有帮助的话。感谢您也建议使用回调来启动图片;我最终使用它来获得平均速度。我会投票赞成你的答案,但我没有足够的代表。
    【解决方案2】:

    所有变量的读取和写入都必须同步。除非该读取是同步的,否则不能保证读取反映变量的当前值,即使该变量被标记为 volatile。

    Goetz 关于并发的书对同步非常清楚:如果有疑问,请同步。如果你还没有这本书,这篇文章会有所帮助:http://www.javaworld.com/article/2075692/java-concurrency/avoid-synchronization-deadlocks.html

    也就是说:

    一般来说,您应该在阅读任何内容时进行同步 以前可能由其他人编写的变量 线程,或者每当您编写任何可能的变量时 随后被另一个线程读取。

    【讨论】:

    • 你说得对,我应该同步读写,谢谢你的文章。但是阻塞的代码不包含synchronized,那么它会阻塞在哪里呢?
    • @Shane_S 也许我误解了这个问题,但我认为您正在尝试读取一个线程上的变量,而该变量正在另一个线程上写入。读取线程上的变量值被缓存,因此您正在读取陈旧数据。您需要同步以确保您在一个线程上的读取与另一线程上的写入同步同步。它并没有真正阻塞,而是卡在一个紧密的循环中,总是读取一个陈旧的“真实”,让它保持在循环中。
    • 这是有道理的,但不知何故 UI 线程也挂起。即使工作线程陷入无限循环,我也不明白这会如何阻塞 UI。
    • @Shane_S 实际上,更仔细地查看您的代码,您的线程调用了主 UI 线程上的方法。这不是处理程序的工作方式。这都是倒退。您需要将循环放在线程中直到停止,然后在完成后发回主 ui 线程。文档非常清楚,发布到 Handler 的 Runnable 将在发布 Runnable 的线程上运行。也就是说,在主 UI 线程上。其他线程中似乎没有任何实际工作。
    • 好吧,你确实有另一个线程,但它所做的只是一个紧密循环中的主线程上的方法。这行不通。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-08-13
    • 2017-07-07
    • 2018-04-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多