在高并发的状况下,你的程序是否是常常出现一些诡异的BUG
,每次都是花费大量时间排查,可是你有没有思考过这一切罪恶的源头是什么呢?java
CPU
、内存
、I/O设备
的速度差别愈来愈大,这也是程序性能的瓶颈,根据木桶理论,最终决定程序的总体性能取决于最慢的操做-读写I/O设备
,单方面的提升CPU的性能是无用的。面试
为了平衡三者的差距,大牛前辈们不断努力,最终作出了卓越的贡献:编程
CPU
增长了缓存,平衡与内存之间的速度差别CPU
,进而均衡 CPU
与 I/O
设备的速度差别;注意:正是硬件前辈们作的这些贡献,额外的后果须要软件工程师来承担,太坑了。缓存
在单核CPU的时代,全部的线程都在单个CPU上执行,不存在CPU数据和内存的数据的一致性。markdown
一个线程对共享变量的修改,另一个线程可以马上看到,咱们称为可见性。并发
由于全部的线程都是在同一个CPU缓存中读写数据,一个线程对缓存的写,对于另一个线程确定是可见的。以下图:编程语言
从上图能够很清楚的了解,线程A对于变量的修改都是在同一个CPU缓存中,则线程B确定是可见的。高并发
可是多核时代的到来则意味着每一个CPU上都有一个独立的缓存,信息再也不互通了,此时保证内存和CPU缓存的一致性就很难了。以下图:性能
从上图能够很清楚的了解,线程A和线程B对变量A的改变是不可见的,由于是在两个不一样的CPU缓存中。优化
最简单的证实方式则是在多核CPU的电脑上跑一个循环相加的方法,同时开启两个线程运行,最终获得的结果确定不是正确的,以下:
public class TestThread {
private Long total=0L;
//循环一万次相加
private void add(){
for (int i = 0; i < 10000; i++) {
total+=1;
}
}
//开启两个线程相加
public static void calc() throws InterruptedException {
TestThread thread=new TestThread();
//建立两个线程
Thread thread1=new Thread(thread::add);
Thread thread2=new Thread(thread::add);
//启动线程
thread1.start();
thread2.start();
//阻塞主线程
thread1.join();
thread2.join();
System.out.println(thread.total);
}
复制代码
上述代码在单核CPU的电脑上运行的结果确定是20000,可是在多核CPU的电脑上运行的结果则是在10000~20000
之间,为何呢?
缘由很简单,第一次在两个线程启动后,会将total=0
读取到各自的CPU缓存中,执行total+1=0
后,各自将获得的结果total=1
写入到内存中(理想中应该是total=2
),因为各自的CPU缓存中都有了值,所以每一个线程都是基于各自CPU缓存中的值来计算,所以最终致使了写入内存中的值是在10000~20000
之间。
注意:若是循环的次数不多,这种状况不是很明显,若是次数设置的越大,则结果越明显,由于两个线程不是同时启动的。
早期的操做系统是基于进程调度CPU,不一样进程间是共享内存空间的,好比你在IDEA写代码的同时,可以打开QQ音乐,这个就是多进程。
操做系统容许某个进程执行一段时间,好比40毫秒,过了这个时间则会选择另一个进程,这个过程称之为任务切换
,这个40毫秒称之为时间片
,以下图:
在一个时间片内,若是一个进程进行IO操做,好比读文件,这个时候该进程能够把本身标记为休眠状态
并让出CPU的使用权,待文件读进内存,操做系统会将这个休眠的进程唤醒,唤醒后的进程就有机会从新得到CPU的使用权。
现代的操做系统更加轻量级了,都是基于线程调度,如今提到的任务切换
大都指示线程切换
。
注意:操做系统进行任务切换是基于
CPU指令
。
基于CPU指令是什么意思呢?Java做为高级编程语言,一条简单的语句可能底层就须要多条CPU指令,例如total+=1
这条语句,至少须要三条CPU指令,以下:
total
从内存读到CPU寄存器中基于CPU指令是什么意思呢?简单的说就是任务切换的时机多是上面的任何一条指令完成以后。
咱们假设在线程A执行了指令1后作了任务切换,此时线程B执行,虽然执行了total+1=1
,可是最终的结果却不是2,以下图:
咱们把一个或者多个操做在CPU执行过程当中不被中断的特性称之为原子性。
注意:CPU仅仅能保证CPU指令执行的原子性,并不能保证高级语言的单条语句的原子性。
此处分享一道经典的面试题:
Long
类型的数据在32位操做系统中加减是否存在并发问题?答案:是,由于Long
类型是64位,在32位的操做系统中执行加减确定是要拆分红多个CPU指令,所以没法保证加减的原子性。
编译优化算是最诡异的一个难题了,虽然高级语言规定了代码的执行顺序,可是编译器有时为了优化性能,则会改变代码执行的顺序,好比a=4;b=3;
,在代码中可能给人直观的感觉是a=4
先执行,b=3
后执行,可是编译器可能为了优化性能,先执行了b=3
,这种对于咱们肉眼是不可见的,上面例子中虽然不影响结果,可是有时候编译器的优化可能致使意想不到的BUG。
双重校验锁实现单例不知你们有没有据说过,代码以下:
public class Singleton {
static Singleton instance;
static Singleton getInstance(){
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
复制代码
这里我去掉了volatile
关键字,那么此时这个代码在并发的状况下有问题吗?
上述代码看上去很完美,可是最大的问题就在new Singleton();
这行代码上,预期中的new
操做顺序以下:
Singleton
对象instance
变量可是实际上编译优化后的执行顺序以下:
instance
变量Singleton
对象不少人问了,优化后影响了什么?
将内存N的地址提早赋值给instance
变量意味着instance!=null
是成立的,一旦是高并发的状况下,线程A
执行第二步发生了任务切换
,则线程B
执行到了 if (instance == null)
这个判断,此时不成立,则直接返回了instance
,可是此时的instance
并无初始化
过,若是此时访问其中的成员变量则会发生空指针异常
,执行流程以下图:
并发编程是区分高低手的门槛,只有深入理解三大特性:可见性
、原子性
、有序性
才能解决诡异的BUG
。
本文分析了带来这三大特性源头,以下:
可见性
问题原子性
问题有序性
问题另外,做者已经完成了两个专栏的文章Mybatis进阶、Spring Boot 进阶 ,已经将专栏文章整理成书,有须要的公众号回复关键词Mybatis 进阶
、Spring Boot 进阶
免费获取。