Java-多线程三大特性

概述

多任务处理在现代计算机操作系统中几乎已是一项必备的功能了。在许多情况下,让计算机同时去做几件事情,不仅是因为计算机的运算能力强大了,还有一个很重要的原因是计算机的运算速度与它的存储和通信子系统速度的差距太大,大量的时间都花费在磁盘I/O、网络通信或者数据库访问上。如果不希望处理器在大部分时间里都处于等待其他资源的状态,就必须使用一些手段去把处理器的运算能力“压榨”出来,否则就会造成很大的浪费,而让计算机同时处理几项任务则是最容易想到、也被证明是非常有效的“压榨”手段。

引用自<深入理解JAVA虚拟机 JVM高级特性与最佳实践>

CPU缓存(Cache Memory)

CPU缓存是位于CPU与内存之间的临时存储器,它的容量比内存小的多但是交换速度却比内存要快得多。高速缓存的出现主要是为了解决CPU运算速度与内存读写速度不匹配的矛盾,因为CPU运算速度要比内存读写速度快很多,这样会使CPU花费很长时间等待数据到来或把数据写入内存。在缓存中的数据是内存中的一小部分,但这一小部分是短时间内CPU即将访问的,当CPU调用大量数据时,就可先缓存中调用,从而加快读取速度。

当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。

CPU缓存可以分为一级缓存,二级缓存,部分高端CPU还具有三级缓存,每一级缓存中所储存的全部数据都是下一级缓存的一部分,这三种缓存的技术难度和制造成本是相对递减的,所以其容量也是相对递增的。当CPU要读取一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找。

cpu缓存带来的问题-缓存不一致

  • 单线程、cpu
    核心的缓存只被一个线程访问。缓存独占,不会出现访问冲突等问题。

  • 单核CPU,多线程
    进程中的多个线程会同时访问进程中的共享数据,CPU将某块内存加载到缓存后,不同线程在访问相同的物理地址的时候,都会映射到相同的缓存位置,这样即使发生线程的切换,缓存仍然不会失效。但由于任何时刻只能有一个线程在执行,因此不会出现缓存访问冲突。

  • 多核CPU,多线程。
    每个核都至少有一个L1 缓存。多个线程访问进程中的某个共享内存,且这多个线程分别在不同的核心上执行,则每个核心都会在各自的caehe中保留一份共享内存的缓冲。由于多核是可以并行的,可能会出现多个线程同时写各自的缓存的情况,而各自的cache之间的数据就有可能不同。

在CPU和主存之间增加缓存,在多线程场景下就可能存在缓存一致性问题,也就是说,在多核CPU中,每个核的自己的缓存中,关于同一个数据的缓存内容可能不一致。

缓存一致性

缓存一致性可以分为三个层级:

  • 在进行每个写入运算时都立刻采取措施保证数据一致性
  • 每个独立的运算,假如它造成数据值的改变,所有进程都可以看到一致的改变结果
  • 在每次运算之后,不同的进程可能会看到不同的值(这也就是没有一致性的行为)

为了解决缓存不一致性问题,通常来说有以下2种解决方法:

  • 通过在总线加LOCK#锁的方式
  • 通过缓存一致性协议

这2种方式都是硬件层面上提供的方式。

在早期的CPU当中,是通过在总线上加LOCK#锁的形式来解决缓存不一致的问题。因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。在总线上发出了LCOK#锁的信号,那么只有等待这段代码完全执行完毕之后,其他CPU才能从其内存读取变量,然后进行相应的操作。这样就解决了缓存不一致的问题。

但是由于在锁住总线期间,其他CPU无法访问内存,会导致效率低下。因此出现了第二种解决方案,通过缓存一致性协议 来解决缓存一致性问题。

最出名的就是Intel的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

三个概念

  • 原子性
    原子性,一个或者多个操作一旦开始之后其过程要么全部成功,要么全部失败,中间不会被打断,所以也不存在中间状态。
    Java中的原子操作包括:

  • 除 long 和 double 之外的基本类型的赋值操作。
    这是由于 long 和 double 都是64位的数据,在32位JVM中对64位的数据的读、写分两步,每一步读或者写32位的数据,这样就会造成两个线程对同一个变量的读写出现一个线程写高32位、另一个线程写入低32位数据。这样此变量的数据就出现不一致的情况。
    非原子性的读、写只是造成long、double类型变量的高、低32位数据不一致。
  • java.concurrent.Atomic.* 包中所有类的一切操作
  • 有序性
    即程序执行的顺序按照代码的先后顺序执行。可是在jvm中很多时候程序并不是按照代码李所写的顺序执行的。

  • 简单解释一下:

    我们都知道程序在编写的时候对于值的变量的定义等都是有一定的编写顺序的。我们期望我们的程序都按照预定的顺序运行。然而当代码实际运行起来的时候就只有cpu能决定运行顺序了,当然整个代码运行的结果肯定是跟预期一样的,但是其过程就不一定了。这种情况对于单线程来说没什么问题,就单线程来说所有数据都是为自己所用,不论过程如何,只要最终结果是我们预期的就可以,中间经过什么样的方式去处理只要能保证效率其实并不重要(但不是说随意执行,一些必要的顺序还是会保持)。但是在多线程的情况下就不一样,因为过程不再是自己的过程很有可能也是别的线程的过程。所以过程的不一样可能会影响到别的线程的过程。

    //在没有对数据进行引用或者操作之前,赋值无关顺序,只要最后每一个值都是对的就可以
    int x = 1int y = 2;
    String hello = "hello";
      
      
    • 1
    • 2
    • 3
    • 4

    修改

    //此时 x 的赋值就必须在 y之前 
    int x = 1int y = x;
    String hello = "hello";
      
      
    • 1
    • 2
    • 3
    • 4

    就好比早上9点打卡上班,无论是坐公交还是地铁都无所谓我只要9点打卡上班,但是假如此时你途中要去某个小店迟早点然后骑自行车打卡上班,这个时候上班就不再是地铁公交无所谓了就需要先选择一个方式到达小店,再骑车上班打卡。至于一开始是地铁还是公交虽然还是无所谓但是从到小店开始后面的就必须是固定下来的顺序。

    关于指令重排推荐一篇文章浅显清晰 http://ifeve.com/jvm-memory-reordering/

    在计算机执行指令的顺序在经过程序编译器编译之后形成的指令序列,一般而言,这个指令序列是会输出确定的结果;以确保每一次的执行都有确定的结果。但是,一般情况下,CPU和编译器为了提升程序执行的效率,会按照一定的规则允许进行指令优化,在某些情况下,这种优化会带来一些执行的逻辑问题,主要的原因是代码逻辑之间是存在一定的先后顺序,在并发执行情况下,会发生二义性,即按照不同的执行逻辑,会得到不同的结果信息。

    Java内存模型中的程序天然有序性可以总结为一句话:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存主主内存同步延迟”现象。
    引用自<深入理解JAVA虚拟机 JVM高级特性与最佳实践>

    我的理解是就线程内部而言最终结果正确即是合理的操作也就可以说是有序的,相对有序,不是绝对按照程序编写的顺序。但从一个线程观察另一个线程的时候,另一个线程的操作本会发生重排,并且是可能随时被打断(原子操作除外)的,这个时候当线程又相互关系的时候就会发生不可知的运行结果,所以可以说是无序的,无法估计的。

    Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile关键字本身就包含了禁止指令重排序的语义,而synchronized则是由“一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则来获得的,这个规则决定了持有同一个锁的两个同步块只能串行地进入。
    引用自<深入理解JAVA虚拟机 JVM高级特性与最佳实践>

  • 可见性

  • 可见性是指当一个线程修改了线程共享变量的值,其它线程能够立即得知这个修改。

    Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方法来实现可见性的,无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是volatile的特殊规则保证了新值能立即同步到主内存,以及每使用前立即从内存刷新。因为我们可以说volatile保证了线程操作时变量的可见性,而普通变量则不能保证这一点。
    除了volatile之外,Java还有两个关键字能实现可见性,它们是synchronized。同步块的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store和write操作)”这条规则获得的,而final关键字的可见性是指:被final修饰的字段是构造器一旦初始化完成,并且构造器没有把“this”引用传递出去,那么在其它线程中就能看见final字段的值。
    引用自<深入理解JAVA虚拟机 JVM高级特性与最佳实践>

    概述

    多任务处理在现代计算机操作系统中几乎已是一项必备的功能了。在许多情况下,让计算机同时去做几件事情,不仅是因为计算机的运算能力强大了,还有一个很重要的原因是计算机的运算速度与它的存储和通信子系统速度的差距太大,大量的时间都花费在磁盘I/O、网络通信或者数据库访问上。如果不希望处理器在大部分时间里都处于等待其他资源的状态,就必须使用一些手段去把处理器的运算能力“压榨”出来,否则就会造成很大的浪费,而让计算机同时处理几项任务则是最容易想到、也被证明是非常有效的“压榨”手段。

    猜你喜欢

    转载自blog.csdn.net/jinjianghai/article/details/90692997