第七章题目是“并发与多线程”,在实际Android开发中,多线程编程是无法避免的,这次跟着书籍来学习下Java里的多线程开发。
由于功耗的缘故,单核CPU的运行速度提升困难,所以英特尔公司开发出了多核CPU,我们现在所用的智能手机,基本都是多核CPU的运行环境。
先说说几个基本概念:并发、并行、线程安全。
并发(Concurrency)
书中定义:并发是指在某个时间段内,多任务交替处理的能力。
在单核CPU时代,CPU一次只能运行一个线程。实际业务里,一个作业可不总是CPU密集型的,必然穿插着大量的IO调用在其中,IO特点就是阻塞等待。如果CPU不做任务切换,那会浪费大量时间在等待IO操作上,所以CPU会轮流的执行多个线程任务,这样可以充分利用CPU。微观上,CPU将一段运行时间分成许多份,然后安排给各个线程轮流运行,造成宏观上所有的线程仿佛同时在执行。单核CPU一段时间处理多个线程任务,就是并发。
并行(Parallelism)
书中定义:并行是指同时处理多任务的能力。
进入多核CPU时代后,多个核心同时运行多个任务,这个就是并行。
两者概念非常容易混淆,我觉得主要还是在“并发”这个词的意思上难以理解,我就不太明白,为什么一段时间能交替执行多个任务,就被叫成“并发”(但我也想不出个词来代替)。目前还是靠多记忆,下面是书中对并行、并发做的例子,很清楚的说明了两者之间的关系:
某个科室有两个专家同时出诊,这是“并行”;其中一个医生,时而问诊,时而查看化验单,然后继续出诊,也会突然中断去处理病人的咨询,这是“并发”。
线程安全
由于各个线程轮流轮流占用CPU计算资源,可能会出现某个线程尚未执行完任务就不得不中断的情况,比如两个线程同时修改一个变量,这时候变量的结果就难以预料,这就是线程不安全。
从代码里知道,两次Runnable里,++count一共应该被执行了2_0000_0000次,然而实际结果却只是1_3509_9389,离2亿还差很远(而且多次运行结果还不太一样),这就是多线程同时修改数据可能会导致的结果。接下来具体分析,先看++count方法:
int count(){
count = count + 1;
return count;
}
++count其实应该看成是三步:
- 先计算一个count+1
- 用第一步计算的结果给count赋值
- 返回count
那么,假如count自增到了100:
- 在线程一里,先计算出count+1 = 101。
- 线程一正准备将101赋值给count时,因为时间片用完,CPU轮转到了线程二。
- 在线程二里,count现在还是100(没来的及赋值),那么经过多次自增操作后,count到了150。
- 此时线程二让出CPU,那线程一会接着前面的运行,将101赋值给count,然而count现在已经是150,这明显是变小了。
由于这种现象的存在,所以count会出现值回退的情况,多出现几次,那自然就远比2亿小了。
看下书里的线程安全实现建议:
- 数据单线程内可见,如果数据只在单线程里操作,那自然是线程安全的,比如说ThreadLocal。
- 只读对象,如果数据只可以读,不可修改,那显然也是安全的,比如说用final修饰。
- 线程安全类,使用一些线程安全的类,比如说StringBuffer
- 同步与锁机制,线程安全的核心理念就是”要么只读,要么加锁“,Java并发包里有不少实现该功能的类,后续也都会慢慢涉及到。