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

概述

在 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 callable = new Callable() {

        public User call() throws Exception {
            Thread.sleep(2000L);
            return new User();
        }
    };

    FutureTask futureTask  = new FutureTask(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 等。这里就不一一展开了。

你可能感兴趣的:(Android)