基本概念

多线程的前世今生

多线程的发展的三个历史阶段:

1.最早出现的计算机主要是为了解决复杂的计算问题,而早期的计算机只能够接受一些特定的指令,当用户在输入这个指令的时候,计算机才会去工作,如果不输入指令,计算机就不会工作,因为计算机本身不会存储指令,很多情况下,计算机都会处于等待状态,并没有真正利用计算机本身的资源。于是进入了批处理操作系统的演变过程。

2.批处理操作系统:用户把需要执行的多个指令写在磁带上,然后让计算机去读取这个磁带执行相应的程序,并把结果输出在另外一个磁带上。

3.虽然批处理这种方式能大大提升计算机资源的利用率,但是会遇到一些问题,比如,操作系统的一个指令阻塞了,CPU会等到这个指令执行完毕后,再去执行下一个指令,这样的话就会使CPU处于等待状态,无法提高资源的利用率。为了解决这个问题,就出现了进程和线程的概念。

现代计算机的硬件架构

java多线程 JedisClusterMaxAttemptsException Java多线程演进史_加载

                                注:为了简便起见,只放了两个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();

        }

    }

}

看下图:

java多线程 JedisClusterMaxAttemptsException Java多线程演进史_加载_02

我们理想中的多线程执行的顺序是这样的;线程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从主内存中加载最新值。