最近遇到的一个Java多线程问题

1. 问题描述

我的code出了一个多线程问题,错误如下:


被同事指出问题出在多线程访问数据上,问题具体如下:项目中线程主要有两个:android自带的UIThread,GLSurfaceView中的GLThread负责渲染场景中所有的图形元素,两个线程同时访问对象内的数据。

代码原型如下:

class GLLinesGroupOverlay extends GLLinesOverlay {
	protected ArrayList 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所致。

2. 问题解决办法

想到了两种思路:

方法1:mLineGroup私有化,取代数据protected的方式是一个final protected数据访问接口,而且接口内对数mLineGroup加锁。不过这么搞粒度过小了吧。

方法2:

class GLLinesGroupOverlay extends GLLinesOverlay {
	protected ArrayList 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的重载函数做里子。架子中考虑了多线程访问问题,里子直接实现逻辑即可。
通过final阻止public接口被重写,protected函数实现接口可以重写,而且不需要也不能加锁(如果有人吃饱了没事干,还要加锁那可能也会有问题)! 

数据被设成protected,希望派生来访问修改,而且希望派生类在加锁的情况下访问。 真心没有万全之策,只能寄希望于写派生类的人头脑清楚点!

3. final后语

Java 数据不想被修改,函数不想被重写 统一采用final。
      final类不能被继承,没有子类,final类中的方法默认是final的。
      final方法不能被子类的方法覆盖,但可以被继承。
      final成员变量表示常量,只能被赋值一次,赋值后值不再改变。
      final不能用于修饰构造方法。
C++ 数据不想被修改const;C++11才开始增加override和final关键字: http://www.devbean.net/2012/05/cpp11-override-final/


你可能感兴趣的:(移动开发,Android,随想&&感想,OpenGL)