用多线程并发处理,目的是为了让程序更充分地利用CPU ,好能加快程序的处理速度和用户体验。如果每个线程各自处理的部分互不相干,那真是极好的,我们在程序主线程要做的同步控制最多也就是等待几个工作线程的执行完毕,如果不 Care 结果的话,连同步等待都能省去,主线程撒开手让这些线程干就行了。
不过,现实还是很残酷的,大部分情况下,多个线程是会有竞争操作同一个对象的情况的,这个时候就会导致并发常见的一个问题--数据竞争(Data Racing)。
这篇文章我们就来讨论一下这个并发导致的问题,以及多线程间进行同步控制和通信的知识,本文大纲如下:
并发导致的Data Racing问题
怎么理解这个问题呢,拿一个多个线程同时对累加器对象进行累加的例子来解释吧。
package com.learnthread;
public class DataRacingTest {
public static void main(String[] args) throws InterruptedException {
final DataRacingTest test = new DataRacingTest();
// 创建两个线程,执行 add100000() 操作
// 创建Thread 实例时的 Runnable 接口实现,这里直接使用了 Lambda
Thread th1 = new Thread(()-> test.add100000());
Thread th2 = new Thread(()-> test.add100000());
// 启动两个线程
th1.start();
th2.start();
// 等待两个线程执行结束
th1.join();
th2.join();
System.out.println(test.count);
}
private long count = 0;
// 想复现 Data Racing,去掉这里的 synchronized
private void add100000() {
int idx = 0;
while(idx++ < 100000) {
count += 1;
}
}
}
上面这个例程,如果我们不启动 th2 线程,只用 th1 一个线程进行累加操作的话结果是 100000。按照这个思维,如果我们启动两个线程那么最后累加的结果就应该是 200000。 但实际上并不是,我们运行一下上面的例程,得到的结果是:
168404
Process finished with exit code 0
当然这个在每个人的机器上的结果是不一样的,而且也是有可能恰好等于 200000,需要多运行几次,或者是多开几个线程执行累加,出现 Data Racing 的几率才高。
程序出现 Data Racing 的现象,就意味着最终拿到的数据是不正确的。那么为了避免这个问题就需要通过加锁来解决了,让同一时间只有持有锁的线程才能对数据对象进行操作。当然针对简单的运算、赋值等操作我们也能直接使用原子操作实现无锁解决 Data Racing, 我们为了示例足够简单易懂才举了一个累加的例子,实际上如果是一段业务逻辑操作的话,就只能使用加锁来保证不会出现 Data Racing了。
加锁,只是线程并发同步控制的一种,还有释放锁、唤醒线程、同步等待线程执行完毕等操作,下面我们会逐一进行学习。
同步控制--synchronized
开头的那个例程,如果想避免 Data Racing,那么就需要加上同步锁,让同一个时间只能有一个线程操作数据对象。 针对我们的例程,我们只需要在 add100000
方法的声明中加上 synchronized
即可。
// 想复现 Data Racing,去掉这里的 synchronized
private synchronized void add100000() {
int idx = 0;
while(idx++ < 100000) {
count += 1;
}
}
是不是很简单,当然 synchronized