概述
在 Android 开发中,常常会遇到这样的需求:主线程用到的成员变量需要在子线程初始化,初始化的过程是异步的,由于 CPU 分配时间片资源是随机的,主线程使用时,该成员变量可能依然是 null,导致空指针。这就是多线程间变量同步的问题。
代码如下:
public class AsyncMemberInitiation {
static User user = null;
public static void main(String[] args) {
new Thread(){
public void run() {
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
user = new User();
System.out.println("user in SubThread: " + user);
};
}.start();
System.out.println("user in MainThread: " + user);
}
}
输出结果:
user in MainThread: null
user in SubThread: User@175fd394
针对这个问题,有下面几种解决方案:
解决方案
FutureTask 代替 Runnable
FutureTask + Callable 是 java.util.concurrent 提供专门处理密集计算的异步任务,FutureTask 实现了 RunnableFuture 接口,RunnableFuture 继承自 Runnable,并增加了一些处理并发的特性。FutureTask 的 get 方法,能够阻塞主线程,直到子线程初始化完成,才继续执行主线程逻辑。(PS:AsyncTask 内部的任务分发,就是通过 FutureTask 实现,有兴趣的朋友可以看 AsyncTask 的构造方法)
FutureTask.get()
Waits if necessary for the computation to complete, and then retrieves its result.
如有必要,最多等待为使计算完成所给定的时间之后,获取其结果(如果结果可用)。
使用如下:
static User sUser = null;
public static void main(String[] args) {
Callable<User> callable = new Callable<User>() {
public User call() throws Exception {
Thread.sleep(2000L);
return new User();
}
};
FutureTask<User> futureTask = new FutureTask<User>(callable);
Thread thread = new Thread(futureTask);
thread.start();
try {
sUser = futureTask.get();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(sUser);
}
结果:
user in MainThread: User@6d06d69c
将初始化和使用进行分离
文中开头的例子无法对两个过程进行明显的分离。但是当结合单例模式使用时,可以很好的分离。下面的例子能能说明问题。(PS:AsyncTask 为了保证 static 的 sHandler 能在主线程,在 ActivityThread.main 中调用了 AsyncTask.init)
代码如下:
public class ImageLoader {
private static volatile ImageLoader mInstance;
/* 后台轮询线程 */
private Thread mPoolThread;
/* 关联后台轮询线程的handler */
private Handler mPoolThreadHandler;
private ImageLoader() {
// 初始化后台轮询线程
mPoolThread = new Thread() {
@Override
public void run() {
mPoolThreadHandler = new Handler();
}
};
mPoolThread.start();
}
public static ImageLoader getSingle() {
if (mInstance == null) {
synchronized (ImageLoader.class) {
if (mInstance == null) {
mInstance = new ImageLoader();
}
}
}
return mInstance;
}
public synchronized void display(final String path, final ImageView imageView) {
if (mPoolThreadHandler == null) {
mSemaphonePoolThreadHander.acquire();
}
mPoolThreadHandler.sendEmptyMessage(0);
}
}
使用如下:
ImageLoader.getSingle().display(url, imageview);
上面是一个简易的 ImageLoader 加载框架的核心类,为了更能说明问题,只保留相关代码。
可以发现 getSingle 后紧跟着 display。而 mPoolThread 在子线程初始化,当第一次使用时,mPoolThread 很可能是 null。此时,我们要求用户在 Activity.onCreate 或者 Application.onCreate 中手动调用 getSinge() 方法。或许是一个不错的解决办法。
于是,代码就变成了这样:
public class ImageLoader {
private static volatile ImageLoader mInstance;
/* 后台轮询线程 */
private Thread mPoolThread;
/* 关联后台轮询线程的handler */
private Handler mPoolThreadHandler;
private ImageLoader() {
// 初始化后台轮询线程
mPoolThread = new Thread() {
@Override
public void run() {
mPoolThreadHandler = new Handler();
}
};
mPoolThread.start();
}
public static ImageLoader getSingle() {
if (mInstance == null) {
synchronized (ImageLoader.class) {
if (mInstance == null) {
mInstance = new ImageLoader();
}
}
}
return mInstance;
}
public static void initialization() {
getSingle();
}
public synchronized void display(final String path,final ImageView imageView) {
if (mPoolThreadHandler == null) {
throw new IllegalStateException(
"ImageLoader must be called initialization() at onCreate() in activity or application.");
}
mPoolThreadHandler.sendEmptyMessage(0);
}
}
使用时:
class BaseActivity extends Activity {
onCreate(){
ImageLoader.initialization();
}
public void onClick(View view){
ImageLoader.getSingle().display(path, imageView);
}
}
嗯,看起来似乎很好的解决了问题,加上 ImageLoader 这样的框架,一般需要在 Application 进行各种配置,我们在配置方法的某个方法处悄悄调 ImageLoader.initialization(); 这样,用户连初始化都省了。尽管如此,对于有代码洁癖的程序员来说,还是不够优雅。于是就有了第三种。
信号量 Semaphore
Semaphore
Semaphore 是一个计数信号量。从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动。拿到信号量的线程可以进入代码,否则就等待。通过acquire() 和 release() 获取和释放访问许可。
简单解释一下,Semaphore 是一个锁的集合,执行 Semaphore 的 acquire 时,会判断当前 Semaphore 里有几把锁,如果锁个数为 0,那么就阻塞,直到 Semaphore 的 release 方法被调用后锁个数 +1,acquire 不再阻塞。
于是,代码就变成了这样:
static User user = null;
static Semaphore semaphore = new Semaphore(0);
public static void main1(String[] args) {
new Thread(){
public void run() {
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
user = new User();
// 初始化完成,释放锁
semaphore.release();
System.out.println("user in SubThread: " + user);
};
}.start();
// 如果锁的个数为0,则阻塞当前线程
try {
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("user in MainThread: " + user);
}
其他解决方案
这个问题的关键就是,子线程初始化完成后需要通知主线程。所以,除了以上几种,还可以使用 android 中的 handler 机制和线程的 wait、notify 等。这里就不一一展开了。