线程同步
1
2
3
4
5
6
7
8
9
10
11
12
13
|
int a = 0;
new Thread(() -> {
for (int i = 0; i < 1000000; i++) {
a++;
}
System.out.println(a);
}).start();
new Thread(() -> {
for (int i = 0; i < 1000000; i++) {
a++;
}
System.out.println(a);
}).start();
|
看如上方法, 看似很简单的方法, 那么问题来了, 输出结果是什么? 事实上每次结果都不一样, 我随便拿一次运行结果:
很显然, 这种情况下多线程结果变得不可控, 事实上在 Android 开发中我们遇到这种情况并不是很多, 因为我们并不像后端那种有庞大的并发请求, 但是遇到这种情况我们也需要知道如何解决, 方法很简单 int 加上关键字 volatile
即可, 这个关键词代表同步锁, 意味着修改这个值将会逐次执行.
这是真的吗? 如果你真的执行之后你就会发现, 其实还是不行, 这其中的原因是因为 ++ 这个方法在 jvm 虚拟机中并不是一步完成的, 而是类似于以下方式(伪代码):
1
2
3
|
val tmp = a
a = tmp + 1
return tmp
|
这三步操作并不是一次性直接完成的, 如果多线程冲突的时候返回值依旧是不正确的, 我们应该使用AtomicInteger, 并使用其 getAndIncrement
这个方法, 我们修改代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
volatile AtomicInteger a = new AtomicInteger(0);
void main() {
new Thread(() -> {
for (int i = 0; i < 1000000; i++) {
a.getAndIncrement();
}
System.out.println(a);
}).start();
new Thread(() -> {
for (int i = 0; i < 1000000; i++) {
a.getAndIncrement();
}
System.out.println(a);
}).start();
}
|
这样执行的结果如下, 符合预期: 其中一个到了 2000000就跳出:
顺带一提, 还有其他 AtomicXXX 之类的方法, 可以实现各种类型的类似作用, 可以自行探索.
Synchronized
还是上面的出错代码, 这时候我们其实还可以采用如下方式解决
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
int a = 0;
synchronized void plus() {
a++;
}
void main() {
new Thread(() -> {
for (int i = 0; i < 1000000; i++) {
plus();
}
System.out.println(a);
}).start();
new Thread(() -> {
for (int i = 0; i < 1000000; i++) {
plus();
}
System.out.println(a);
}).start();
}
|
这时我将 ++ 的操作扔到了一个函数体内, 并对其添加了 synchronized
关键词, 这个关键词意思是很简单, 就是这个代码块被多个线程调用的时候会只有一个执行, 其他等待执行, 我们称这个为 锁 🔒
说到这里, 其实还有一个 synchronized
代码块, 用法就是代码块内的方法会保证同时只会有一个在执行, 就比如说以下两种方式是等价的:
1
2
3
4
5
6
7
8
9
10
|
synchronized void plus() {
a++;
}
void plus() {
synchronized (this) {
a++;
}
}
|
这时候问题来了, 这个地方 synchronized 里面填这个 this 是图啥呢? 原因很简单, 我们可以认为 this 就是给这个锁 🔒 起个名字, 这样就更好理解了, 遇到相同名字的时候就保证多线程排队等待执行
这玩意可以是任意对象, 例如:
1
2
3
4
|
final Object monitor = new Object(); // 建议弄成 finial 的
synchronized (monitor) { // 这个锁叫 monitor
a++;
}
|
有了这种魔幻操作就会出现接下来的问题
wait() / notifyAll()
这两个东西任何一个 Object 都有, 他们存在有什么作用呢? 事实上只有当 Object 是锁 🔒 的名字的时候, wait() 和 notify/notifyAll 才会发挥他们的作用
先看什么情况我们会用到这俩东西:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
static int a = 0;
static int b = 0;
public static void main(String[] args) {
new Thread(() -> {
try {
Thread.sleep(1000);
a = 1;
} catch (InterruptedException e) {
}
}).start();
new Thread(() -> {
try {
Thread.sleep(2000);
b = 2;
} catch (InterruptedException e) {
}
}).start();
}
|
假设这是两个耗时请求, a,b 分别代表从数据库等耗时操作读取出来的两个数据, 我们需要将这两个数据进行拼接进行其他请求, 而且事实上我们无法预知请求的操作时间
为保证能拼接起两个请求, 我们就只能顺序执行, 而这样不就是垃圾代码了吗? 明明能异步操作的方法我们写成了同步方法, 白白浪费了性能! 事实上我们可以做如下操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
static boolean a = true;
System.out.println(System.currentTimeMillis() + " lockTest-start");
new Thread(() -> {
try {
Thread.sleep(1000);
System.out.println(System.currentTimeMillis() + " task1 done");
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (monitor) {
try {
while (a) {
monitor.wait();
}
System.out.println(System.currentTimeMillis() + " tasks done");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
synchronized (monitor) {
try {
Thread.sleep(1300);
System.out.println(System.currentTimeMillis() + " task2 done");
a = false;
monitor.notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
|
代码很长, 其实有一大堆无意义的 try catch 操作, 我们对其隐藏:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
static boolean a = true;
System.out.println(System.currentTimeMillis() + " lockTest-start");
new Thread(() -> {
Thread.sleep(1000);
System.out.println(System.currentTimeMillis() + " task1 done");
synchronized (monitor) {
while (a) {
monitor.wait();
}
System.out.println(System.currentTimeMillis() + " tasks done");
}
}).start();
new Thread(() -> {
synchronized (monitor) {
Thread.sleep(1300);
System.out.println(System.currentTimeMillis() + " task2 done");
a = false;
monitor.notifyAll();
}
}).start();
|
输出如下:
1
2
3
4
|
1615465684000 lockTest-start
1615465685048 task1 done
1615465685363 task2 done
1615465685363 tasks done
|
其实逻辑很简单, 线程 1 耗时操作1s, 线程 2 耗时操作 1.3s, 如果顺序执行我们需要 2.3s, 但如果我们采用上面的方式只需 1.3s. 现在我来解释一下 wait() 和 notifyAll() 都是啥
wait(long timeout)
在 synchronized 函数体中执行, 执行到这个方法时, 当前 synchronized 函数体 暂时解锁
然后允许在排序中的其他同名的 synchronized 函数体执行并等待返回结果 (timeout为最大超时, 不填就一直等)
具体怎么用可以看上面
notify / notifyAll
和 wait() 配套使用, 如果 notify 之前没 wait() 会抛异常, 执行效果是通知一个已经处于 暂时解锁
状态的 synchronized 代码块, 使他们进入排队状态(注意!!
不是直接执行, 而是进入排队状态)
因此, 我更推荐你使用 notifyAll, 会通知全部在 暂时解锁
状态的代码块.
具体怎么用可以看上面