基本概念
多线程的前世今生
多线程的发展的三个历史阶段:
1.最早出现的计算机主要是为了解决复杂的计算问题,而早期的计算机只能够接受一些特定的指令,当用户在输入这个指令的时候,计算机才会去工作,如果不输入指令,计算机就不会工作,因为计算机本身不会存储指令,很多情况下,计算机都会处于等待状态,并没有真正利用计算机本身的资源。于是进入了批处理操作系统的演变过程。
2.批处理操作系统:用户把需要执行的多个指令写在磁带上,然后让计算机去读取这个磁带执行相应的程序,并把结果输出在另外一个磁带上。
3.虽然批处理这种方式能大大提升计算机资源的利用率,但是会遇到一些问题,比如,操作系统的一个指令阻塞了,CPU会等到这个指令执行完毕后,再去执行下一个指令,这样的话就会使CPU处于等待状态,无法提高资源的利用率。为了解决这个问题,就出现了进程和线程的概念。
现代计算机的硬件架构
注:为了简便起见,只放了两个CPU核心。
如图中所示:现代计算机硬件架构中一般包含两个甚至更多个CPU核心,为了消除CPU和内存之间的速度差异(从内存中加载数据和指令的速度和CPU的执行速度差很多很多),提高指令执行效率,每个CPU核心内建了高速寄存器和三级缓存(L1,L2,L3),其中寄存器分为指令寄存器和数据寄存器两部分。
缓存一致性问题
来源:
当一个线程在CPU上执行的时候,CPU会先将数据和指令从内存中读取加载(load)到缓存中,然后进行计算,计算完成后将数据先写到缓存中,当执行完成后,再将缓存中的值更新到主内存中。当计算机为多核CPU时,就会出现缓存一致性问题。
举个例子:多线程环境中进行i++操作。
public class Test {
private static int i = 1;
public static void main(String... args) {
for(int j = 0; j < 100; j++) {
new Thread(()->{
Test .i ++;
}).start();
}
}
}
看下图:
我们理想中的多线程执行的顺序是这样的;线程1先将i加载到缓存中,CPU执行计算将i+1为2,并将缓存中的i赋值为2,最后将缓存中的i更新到主内存中,此时主内存中i为2,线程2此时再将i加载到缓存中,CPU执行计算将i+1为3,并将缓存中的i赋值为3,最后将缓存中的i更新到主内存中,此时主内存中i为3.符合预期。
而现实中,CPU是并行执行的,如果不做同步处理,很有可能打破上述的理想执行过程,导致。在本例子中,有可能出现CPU1执行完step2,但是还未来得及执行step3,;此时CPU0已经执行step4,将i加载到高速缓存中了,此时高速缓存中i的值还是1,是一个过期的值,那么当CPU0、CPU1执行完step1-step6之后,主内存中的i的值可能为2,而不是3,不符合预期,这就是所谓的线程安全问题。
现代计算机中缓存一致性问题的解决方案
现代计算机中解决多核CPU环境中缓存一致性问题的解决方案有两种;一种是总线锁,另一种是MESI协议(一种缓存一致性协议)。
总线锁的运作机制:当缓存在使用设定为需要保证缓存一致性的共享变量时,锁定这个变量,保证变量只能被当前CPU使用,其他需要使用该变量的CPU需要等待当前CPU执行完毕才能继续执行。是早期的多核CPU架构中使用的一种缓存一致性机制,由于锁定总线的方式会导致CPU等待,所以性能比较低下。而MESI是一种乐观锁的实现:当一个CPU使用某个设定为需要保证缓存一致性的变量时,会给这个变量设置一个内存屏障,当其他CPU读取这个变量时,由于内存屏障的存在,会导致CPU等,直到当前CPU使用完成后,将缓存中的值保存到主内存中,然后通过MESI协议通知其他CPU从主内存中加载最新值。