Android 多线程间变量同步的问题

时间:2022-05-06 07:42:17

概述

在 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 等。这里就不一一展开了。