我的code出了一个多线程问题,错误如下:
被同事指出问题出在多线程访问数据上,问题具体如下:项目中线程主要有两个:android自带的UIThread,GLSurfaceView中的GLThread负责渲染场景中所有的图形元素,两个线程同时访问对象内的数据。
代码原型如下:
class GLLinesGroupOverlay extends GLLinesOverlay { protected ArrayList<GLLinesOverlay> mLineGroup; // call in UIThread public void populate() { synchronized (mLineGroup) { // 初始化数据 } } // call in UIThread public boolean onTap(float x, float y) { synchronized (mLineGroup) { // 点击事件处理 } } // call in GLThread public void draw(GL10 gl) { synchronized (mLineGroup) { // 绘制 } } } class GLMultipleAAOverlay extends GLLinesGroupOverlay { @Override public void populate() { // ##### 操作mLineGroup,忘记加锁啦 ##### } }基类中对数据成员的操作都进行了加锁,写派生类的时候头脑不是特别清楚了,重写了方法但是忘记加锁,而且GLMultipleAAOverlay的构造和调用也很奇葩,代码如下:
// UI线程中调用构造overlay,添加到GL渲染队列中 GLMultipleAAOverlay mtOverlay = new GLMultipleAAOverlay(); addOverlay(mtOverlay); // 添加到GL渲染队列中 // ##### 时序问题 ##### mtOverlay.populate(); // 初始化数据。new了mtOverlay对象立刻添加到渲染队列中,addOverlay(mtOverlay)函数返回后 GLThread线程就不断调用该overlay的draw函数,然后UI线程再调用mtOverlay.populate()函数结果就悲催了, 随机性crash,而且不同手机上行为不一致,在我的HTC G10上一直没crash过,在同事的手机上频率很高。
如果调用代码改成:
// UI线程中调用构造overlay,populate完成后添加到GL渲染队列中 GLMultipleAAOverlay mtOverlay = new GLMultipleAAOverlay(); mtOverlay.populate(); // 初始化数据。 addOverlay(mtOverlay); // 添加到GL渲染队列中这么调用populate完成后GL渲染才开始,不会有并发访问问题,然而确多了一个坑: 如果GLMultipleAAOverlay重写onTap函数,而且忘记加锁就会有并发访问。
多线程本身是跟函数调用时序无关的,所以不能把希望季寄托于GLLinesGroupOverlay的所有函数被以某种正确的时序调用,多线程坑的根因出在派生类可以重写基类函数,任意访问数据成员mLineGroup所致。
想到了两种思路:
方法1:mLineGroup私有化,取代数据protected的方式是一个final protected数据访问接口,而且接口内对数mLineGroup加锁。不过这么搞粒度过小了吧。
方法2:
class GLLinesGroupOverlay extends GLLinesOverlay { protected ArrayList<GLLinesOverlay> mLineGroup; // public final interface function // call in UIThread public final void populate() { synchronized (mLineGroup) { // 初始化数据 populateImpl(); } } // call in UIThread public final boolean onTap(float x, float y) { synchronized (mLineGroup) { onTapImpl(x, y); } } // call in GLThread public final void draw(GL10 gl) { synchronized (mLineGroup) { drawImpl(gl); } } // protected function implementation // ##### 这些函数可以被重写,但是不能加锁 ##### proteced void populateImpl() { // 操作数组,初始化数据 } protected boolean onTapImpl(float x, float y) { // 操作数组,点击事件处理 } protected boolean void drawImpl(GL10 gl) { // 操作数组,绘制 } } class GLMultipleAAOverlay extends GLLinesGroupOverlay { @Override protected void populateImpl() { // ~~~~~ 操作mLineGroup,不用加锁 ~~~~~ } }public final函数做架子,protected的重载函数做里子。架子中考虑了多线程访问问题,里子直接实现逻辑即可。
数据被设成protected,希望派生来访问修改,而且希望派生类在加锁的情况下访问。 真心没有万全之策,只能寄希望于写派生类的人头脑清楚点!