最近看了看 Matrix 的源码,无意间看到一个单例的写法。因为这种写法比较特殊,所以花了些时间认真看了下。发现还真的有点问题。当然,其实问题也不大。因为这种单例的问题在没有并发的情况下出现的概率比较低。所以我说“较个真”而已。
问题出现在 FrameDecorator 获取实例的静态方法。看这个写法的意思就是获取一个单例的 FrameDecorator. 不过,因为这里在创建单例的时候需要用到 FloatFrameView 这个控件。因为在 Android 中存在这样一个限制:只有创建一个 View 的线程才能对它进行修改(并不强制是主线程)。所以,如果我们想要在主线程当中使用这个控件就要求创建 View 的时候就应该在主线程中创建。
从这个方法的逻辑可以看出,这里希望,当当前线程是主线程的时候直接创建 FrameDecorator 实例,当当前线程不是主线程的时候就将创建 FrameDecorator 的操作 post 到主线程中执行。
public static FrameDecorator getInstance(final Context context) {
if (instance == null) {
if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
instance = new FrameDecorator(context, new FloatFrameView(context));
} else {
try {
synchronized (lock) {
mainHandler.post(new Runnable() {
@Override
public void run() {
instance = new FrameDecorator(context, new FloatFrameView(context));
synchronized (lock) {
lock.notifyAll();
}
}
});
lock.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
return instance;
}
这里通过可重入锁 synchronized 实现私有锁进行局部加锁以降低锁粒度。
这里有一个问题,当子线程通获取到 lock 的锁的时候,主线程在 synchronized 的时候是如何获取到锁的呢?这是因为当在子线程中调用 wait 方法的时候,会阻塞并且会同时释放 lock 的锁。所以,主线程当中可以通过 synchronized 获取到 lock 的锁。
经常拿来和 wait 做对比的 sleep 虽然在被调用的时候一样可以让当前线程阻塞,但是不会释放锁的,并且只能通过中断的方式结束阻塞。这是两者之间的区别。
写个例子对比一下,
new Thread(() -> {
try {
synchronized (lock) {
new Thread(() -> {
synchronized (lock) {
System.out.println("do business in child thread."); // 2
lock.notifyAll();
}
}).start();
System.out.println("do business in main thread before wait."); // 1
lock.wait();
System.out.println("do business in main thread after wait."); // 3
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
执行顺序如上述代码所示,即调用 wait 的时候锁被释放,此时,子线程获取到锁,2 被执行,然后 notify 被调用,阻塞被移除,3 被调用。
换成 sleep,
new Thread(() -> {
try {
synchronized (lock) {
new Thread(() -> {
synchronized (lock) {
System.out.println("do business in child thread."); // 3
}
}).start();
System.out.println("do business in main thread before wait."); // 1
Thread.sleep(1_000);
System.out.println("do business in main thread after wait."); // 2
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
锁不会释放,执行结果是,3 必须在 2 完成之后,锁被释放了才能执行。即,
do business in main thread before wait.
do business in main thread after wait.
do business in child thread.
基于 wait+notifyAll 的思路似乎没有什么问题。那么这种单例写法是否存在线程安全问题?答案是会。
这主要的原因是,
-
第一,虽然 `mainHandler.post()` 的逻辑位于 `wait()` 的前面,但是不能因此而断定 `mainHandler.post()` 内的逻辑就一定在 `wait()` 之前被执行。因为 Handler 是一个消息队列机制,消息本身是需要排队的。可以将 Handler 看作一个单线程的线程池,不过即便如此也无法保证任务被加入到线程池之后立即执行。
-
第二,当调用 `wait()` 方法释放锁的时候,此时,主线程和子线程对 lock 处于竞争状态。也就是说,此时子线程仍然有可能竞争到锁而进入同步代码块。也就是,可能会向主线程当中 post 两条创建实例的消息。因此,也就会出现创建多个单例的情况。
我们可以写一个测试程序来模拟下,
public class LockTest {
private static final Object lock = new Object();
private static final Executor executor = Executors.newSingleThreadExecutor();
private static LockTest instance;
public static void main(String...args) {
for (int i=0; i<200; i++) {
new Thread(() -> System.out.println(getInstance())).start();
}
}
private static LockTest getInstance() {
if (instance == null) {
try {
synchronized (lock) {
executor.execute(() -> {
instance = new LockTest();
synchronized (lock) {
lock.notifyAll();
}
});
lock.wait();
}
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
return instance;
}
}
这里通过单例的线程池来模拟消息被 post 到主线程的情况。输出的部分结果如下。也就是会出现线程安全问题的,
...
me.shouheng.LockTest@776d8031
me.shouheng.LockTest@776d8031
me.shouheng.LockTest@735037e6
me.shouheng.LockTest@59a30351
me.shouheng.LockTest@59a30351
me.shouheng.LockTest@31d0f5c9
me.shouheng.LockTest@31d0f5c9
me.shouheng.LockTest@735037e6
me.shouheng.LockTest@735037e6
me.shouheng.LockTest@735037e6
...
那么如何解决呢? 只需要在实例化的时候做一层判断就可以了。为什么呢?因为实例化操作总是在主线程中执行,也就是说实例化的操作是线性执行的。因此是不存在线程安全问题的,
private static LockTest getInstance() {
if (instance == null) {
try {
synchronized (lock) {
executor.execute(() -> {
if (instance == null) {
instance = new LockTest();
}
synchronized (lock) {
lock.notifyAll();
}
});
lock.wait();
}
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
return instance;
}
这种写法的输出结果都是同一个实例,就不贴出来了。
这种单例写法在安卓当中出现问题的概率并不大。我们开发当中应该会有机会遇到这种单例的情况(需要在主线程当中闯将 view 的时候)。不过根据墨菲定律——如果事情有变坏的可能,不管这种可能性有多小,它总会发生。写代码还是要严谨的,估计吃过亏的都会懂。
如何入门学习网络安全
零基础入门
对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。
同时每个成长路线对应的板块都有配套的视频提供:
因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取
优快云大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享
视频配套资料&国内外网安书籍、文档&工具
当然除了有配套的视频,同时也为大家整理了各种文档和书籍资料&工具,并且已经帮大家分好类了。
因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取