最近看了看 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 的时候)。不过根据墨菲定律——如果事情有变坏的可能,不管这种可能性有多小,它总会发生。写代码还是要严谨的,估计吃过亏的都会懂。
题外话
黑客&网络安全如何学习
今天只要你给我的文章点赞,我私藏的网安学习资料一样免费共享给你们,来看看有哪些东西。
1.学习路线图
攻击和防守要学的东西也不少,具体要学的东西我都写在了上面的路线图,如果你能学完它们,你去就业和接私活完全没有问题。
2.视频教程
网上虽然也有很多的学习资源,但基本上都残缺不全的,这是我们和网安大厂360共同研发的网安视频教程,之前都是内部资源,专业方面绝对可以秒杀国内99%的机构和个人教学!全网独一份,你不可能在网上找到这么专业的教程。
内容涵盖了入门必备的操作系统、计算机网络和编程语言等初级知识,而且包含了中级的各种渗透技术,并且还有后期的CTF对抗、区块链安全等高阶技术。总共200多节视频,200多G的资源,不用担心学不全。
因篇幅有限,仅展示部分资料,需要见下图即可前往获取
🐵这些东西我都可以免费分享给大家,需要的可以点这里自取👉:网安入门到进阶资源
3.技术文档和电子书
技术文档也是我自己整理的,包括我参加大型网安行动、CTF和挖SRC漏洞的经验和技术要点,电子书也有200多本,由于内容的敏感性,我就不一一展示了。
因篇幅有限,仅展示部分资料,需要见下图即可前往获取
🐵这些东西我都可以免费分享给大家,需要的可以点这里自取👉:网安入门到进阶资源
4.工具包、面试题和源码
“工欲善其事必先利其器”我为大家总结出了最受欢迎的几十款款黑客工具。涉及范围主要集中在 信息收集、Android黑客工具、自动化工具、网络钓鱼等,感兴趣的同学不容错过。
还有我视频里讲的案例源码和对应的工具包,需要的话见下图即可前往获取
🐵这些东西我都可以免费分享给大家,需要的可以点这里自取👉:网安入门到进阶资源
最后就是我这几年整理的网安方面的面试题,如果你是要找网安方面的工作,它们绝对能帮你大忙。
这些题目都是大家在面试深信服、奇安信、腾讯或者其它大厂面试时经常遇到的,如果大家有好的题目或者好的见解欢迎分享。
参考解析:深信服官网、奇安信官网、Freebuf、csdn等
内容特点:条理清晰,含图像化表示更加易懂。
内容概要:包括 内网、操作系统、协议、渗透测试、安服、漏洞、注入、XSS、CSRF、SSRF、文件上传、文件下载、文件包含、XXE、逻辑漏洞、工具、SQLmap、NMAP、BP、MSF…
因篇幅有限,仅展示部分资料,需要见下图即可前往获取
🐵这些东西我都可以免费分享给大家,需要的可以点这里自取👉:网安入门到进阶资源
————————————————
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。