计算机发展过程中存在一个核心矛盾:CPU、内存与I/O设备,这三者的速度差异。
形象比喻:
1.CPU是天上一天,内存地上一年(假设CPU执行普通指令需要一天,那么CPU读写内存需要一年)。
2.内存是天上一天,I/O是地上十年。
总结:根据木桶原理(一只桶能装多少水取决于最短的那块木板),
程序整体性能取决于最慢的I/O设备。单方面提升CPU性能是无效的。
(类比机械硬盘与固态硬盘对性能的提升。只更换为机械硬盘,内存和CPU不变,你的电脑都能有很大改善。)
如何平衡速度差异:
1.CPU加缓存----------平衡与内存速度差异。
举例:(数组在内存中是占据连续的内存空间的,而CPU在从内存中读取数据的时候会把该内存地址后面的一部分数据也
缓存进去。这样CPU在访问数组数据的时候先从CPU缓存的数组中寻找,找不到再从内存中复制。这也就是CPU缓存的意义,)
2.操作系统增加了进程、线程以分时复用CPU-------平衡CPU和I/O设备的速度差异。
3.编译程序优化指令执行次序,使得缓存利用更加合理。
缓存导致的可见性问题
在如今的多核时代,每科CPU都有自己的缓存,当多个线程在不同CPU上执行,这些线程操作的是不同的缓存。某个线程对共享变量的操作,会出现对另外线程不可见的情况。
public class Test {
private long count = 0;
private void add10K() {
int idx = 0;
while(idx++ < 10000) {
count += 1;
}
}
public static long calc() {
final Test test = new Test();
// 创建两个线程,执行 add() 操作
Thread th1 = new Thread(()->{
test.add10K();
});
Thread th2 = new Thread(()->{
test.add10K();
});
th1.start();
th2.start();
th1.join();
th2.join();
return count; //最终结果会是10000到200000之间的随机数
}
}
线程切换带来的原子性问题
时间片和任务切换简单理解:
1.操作系统运行某个进程执行一小段时间,假如50毫秒,过了50毫秒后操作系统会选择新的进程执行(称之为“任务切换”)
,这50毫秒称为时间片。
时间片.png
线程调度
计算机通常只有一个CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU
的使用权才能执行指令。所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获
得CPU的使用权,分别执行各自的任务。在运行池中,会有多个处于就绪状态的线程在等
待CPU,JAVA[虚拟机]的一项任务就是负责线程的调度,线程调度是指按照特定机制为多
个线程分配CPU的使用权。
参考百度百科:https://baike.baidu.com/item/%E7%BA%BF%E7%A8%8B%E8%B0%83%E5%BA%A6/10226112
调度模型:分时调度模型和抢占式调度模型。
线程调度模型.png
放弃CPU使用权的原因:
1.java虚拟机让当前线程暂时放弃CPU,转到就绪状态,使其它线程获得运行机会。
2.当前线程因为某些原因而进入阻塞状态。
3. 线程结束运行。
放弃cpu的原因.png
bug源之一:任务切换------非原子性。(操作系统做任务切换,可以发生在任何一条CPU指令执行完,而不是高级语言的一条指令。)
以代码 count+=1为例子。
指令1:把变量count加载到CPU寄存器。
指令2:在寄存器执行+1操作。
指令3:将结果写入内存。(缓存机制可能导致写入的是CPU缓存。)
图示:当两个线程由于任务切换,出现这种情况。线程B没有在线程A的结果基础上进行操作。也就是说线程A count+=1不具备原子性。会导结果异常。
线程切换.png
编译器优化:有序性问题
编译器为了优化性能,有时候会改变程序语句执行顺序。例如 a = 6; b = 7这样的顺序可能有化成b=7;a=6;大多时候不影响程序最终结果。不过有时候编译器及解释器的优化会导致意想不到的BUG。
以java中经典的一个单例模式写法为例。
public class Singleton {
static Singleton instance;
static Singleton getInstance(){//获取单例
if (instance == null) { //首先判断是否为空
synchronized(Singleton.class) {//为空就加锁
if (instance == null)//并再次检查instance是否为空
instance = new Singleton();//如果空就创建实例
}
}
return instance;
}
}
解释双重检查目的,避免每次都进行加锁操作。
java中的new操作
1.分配一块内存M。
2.在内存M上初始化Singleton对象。
3.将M的地址赋值给instance对象。
优化后会变成这样:
1.分配一块内存M。
2.将M的地址赋值给instance对象。
3.在内存M上初始化Singleton对象。
上面的new操作在多线程中会出现这样的情况。
public class Singleton {
static Singleton instance;
static Singleton getInstance(){
if (instance == null) { //线程B刚执行这,发现不为空,立即返回,而线程A由于优化了new的执行顺序还没有真正
//的初始化。这时会导致空指针异常。
synchronized(Singleton.class) {
if (instance == null)
instance = new Singleton(); }
}
return instance;
}
}
并发涉及的知识面挺广的,推荐阅读:http://gk.link/a/103WI
网友评论