目录

Java 多线程(二) 线程同步Synchronized, Object.wait() / notifyAll()

线程同步

 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();

看如上方法, 看似很简单的方法, 那么问题来了, 输出结果是什么? 事实上每次结果都不一样, 我随便拿一次运行结果:

1
2
1380648
1832829

很显然, 这种情况下多线程结果变得不可控, 事实上在 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就跳出:

1
2
1964430
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
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, 会通知全部在 暂时解锁 状态的代码块. 具体怎么用可以看上面