并发三大特性

并发和并行

并行

指在同一时刻,有多条指令在多个处理器下执行,存在于多处理器的系统。

并发

指在同一时刻只有一条指令能被执行,但在宏观的角度来看,可能因为多个进程轮换执行,看起来就像是多个进程同时执行,多处理器和处理器的系统都有。

并发三大特性

原子性、有序性、可见性。

可见性

保证可见性的方式

  • 使用volatile关键字
  • 使用内存屏障
  • 使用syschronized关键字
  • 使用Lock
  • 使用final关键字

有序性

程序执行的时候按照代码的先后顺序,因为JVM中存在指令重排。

保证有序性的方式

  • 使用volatile关键字
  • 使用内存屏障
  • 使用syschronized关键字
  • 使用Lock

原子性

一个或多个操作,要不全部执行且不被打断,要不就全部不执行。

保证原子性的方式

  • 使用syschronized关键字
  • 使用Lock
  • 使用CAS

内存中被的共享变量,在被一个线程修改后,其他线程也能看见修改后的值。

实现机制:在共享变量的值被修改后,会将修改后的值写入主内存,而其他线程在读取的时候先刷新

JMM内存模型

描述的是一种抽象的,逻辑上存在的规则,通过这个规则来限定程序中各个变量在共享数据的区域和私有数据的区域以怎样的方式进行访问,规定了一个线程什么时候可以看到由其他线程修改后的共享变量的值,以及如何同步去访问共享变量。

内存交互操作

即将主内存中的共享变量同步到线程中的本地内存,以及从线程中本地内存将修改后的变量同步到主内存的实现,具体分为八个步骤:

  • lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
  • unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引 擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主 内存中,以便随后的write的操作。
  • write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中

交互操作的原则

JMM的内存模型规定在进行以上的操作时,必须要遵从如下原则

  1. 主内存同步到工作内存,必须先read后load;而工作内存同步到主内存,必须先store然后write。但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。
  2. 不允许read和load、store和write操作之一单独出现
  3. 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
  4. 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步到主内存中。
  5. 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过 了assign和load操作。
  6. 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现
  7. 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
  8. 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和 write操作)。

缓存一致性协议

CPU高速缓存

是cpu和主内存之间的一种空间很小的存储器,用来存储CPU刚使用过或频繁使用的一部分数据,当cpu在此使用这部分数据的时候们可以直接从cache中取而不用从主内存中加载,达到提高CPU效率的目的。

一致性协议种类

  • MSI protocol, the basic protocol from which the MESI protocol is derived.
  • Write-once (cache coherency), an early form of the MESI protocol.
  • MESI protocol
  • MOSI protocol
  • MOESI protocol
  • MESIF protocol
  • MERSI protocol
  • Dragon protocol
  • Firefly protocol

MESI协议

MESI协议是一个基于写失效的缓存一致性协议,是支持回写(write-back)缓存的最常用协议。也称作伊利诺伊协议 (Illinois protocol,因为是在伊利诺伊大学厄巴纳-香槟分校被发明的)。与写通过(write through)缓存相比,回写缓冲能节约大量带宽。总是有“脏”(dirty)状态表示缓存中的数据与主存中不同。MESI协议要求在缓存不命中(miss)且数据块在另一个缓存时,允许缓存到缓存的数据复制。与MSI协议相比,MESI协议减少了主存的事务数量。这极大改善了性能。

已修改Modified (M)

缓存行是脏的,与主存的值不同。如果别的CPU内核要读主存这块数据,该缓存行必须回写到主存,状态变为享(S).

独占Exclusive (E)

缓存行只在当前缓存中,但是干净的--缓存数据同于主存数据。当别的缓存读取它时,状态变为共享;当前写数据时,变为已修改状态。

共享Shared (S)

缓存行也存在于其它缓存中且是未修改的。缓存行可以在任意时刻抛弃。

无效Invalid (I)

缓存行是无效的

一致性协议生效过程

伪共享问题

多个核的线程在操作同一个缓存行中的不同变量数据,会出现频繁的缓存失效,即使在代码层面看这两个线程操作的数据之间完全没有关系,但这种不合理得竞争会浪费cpu资源,降低性能 。

 

package com.tuling.jucdemo.jmm;

import sun.misc.Contended;

/**
 * 伪共享
 */
public class FalseSharingTest {

    public static void main(String[] args) throws InterruptedException {
        testPointer(new Pointer());
    }
    private static void testPointer(Pointer pointer) throws InterruptedException {
        long start = System.currentTimeMillis();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100000000; i++) {
                pointer.x++;
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100000000; i++) {
                pointer.y++;
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        // 思考:x,y是线程安全的吗?
        System.out.println(pointer.x+","+pointer.y);
        System.out.println(System.currentTimeMillis() - start);
    }
}
class Pointer {
      long x;
      long y;
}

运行结果

使用volatile之后

出现上述情况的原因就是因为伪共享问题--频繁的缓存失效。

解决的方式

1.将缓存行进行填充,将互相影响的两个变量区分开来,不放入同一个缓存行

2.使用@Contended注解

总线锁定

根据处理器传达的一个lock指令,将整个总线锁定,导致其他指令无法访问。

缓存锁定

因为总线锁定,导致其他的处理器无法和内存进行交互,因此,缓存锁定的作用是在内存区域被缓存在处理器的缓存行上,当被执行引擎修改写回到主内存的时候,处理器不会再总线上进行lock的命令,而是在配合缓存一致性的基础上,修改内存地址,以此来保证操作的原子性。缓存锁定也存在不能使用的情况,一是当操作的数据不能被缓存在处理器内部,或者操作的数据跨多个缓存行时;二是在一些不支持缓存锁定的处理器中。

处理器使用三种相互依赖的机制来执行锁定的原子操作

  • 有保证的原子操作
  • 总线锁定,使用LOCK#信号和LOCK指令前缀
  • 缓存一致性协议,确保原子操作可以在缓存的数据结构上执行(缓存锁);这种机制出现在Pentium 4、Intel Xeon和P6系列处理器中

总线窥探

是为了在分布式系统中维护缓存一致性而产生的一种缓存控制监听机制。

工作原理

当数据同时被多个处理器共享,其中一个处理器对数据进行了修改,更改必须广播到其他使用该数据作为数据副本的缓存行中。

指令重排序

Java语言规范规定JVM线程内部维持顺序化语义。即在保持程序运行结果一致的前提下按最优(cpu最大运行效率)的执行顺序对代码排序后执行,此过程叫指令的重排序。

指令重排序的意义:JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的对机器指令进行重 排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。

volatile重排序规则

操作1

操作2

能否重排序

普通读/写

volatile读

volatile写

普通读/写

volatile读

volatile写

volatile禁止重排序场景:

1. 第二个操作是volatile写,不管第一个操作是什么都不会重排序

2. 第一个操作是volatile读,不管第二个操作是什么都不会重排序

3. 第一个操作是volatile写,第二个操作是volatile读,也不会发生重排序

JVM层面的内存屏障

LoadLoad屏障:(指令Load1; LoadLoad; Load2),在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。

LoadStore屏障:(指令Load1; LoadStore; Store2),在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。

StoreStore屏障:(指令Store1; StoreStore; Store2),在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。

StoreLoad屏障:(指令Store1; StoreLoad; Load2),在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能

由于x86只有store load可能会重排序,所以只有JSR的StoreLoad屏障对应它的mfence或lock前缀指令, 其他屏障对应空操作。

JMM内存屏障插入策略

1. 在每个volatile写操作的前面插入一个StoreStore屏障

2. 在每个volatile写操作的后面插入一个StoreLoad屏障

3. 在每个volatile读操作的后面插入一个LoadLoad屏障

4. 在每个volatile读操作的后面插入一个LoadStore屏障

硬件层内存屏障

硬件层提供了一系列的内存屏障 memory barrier / memory fence(Intel的提法)来提供一致性的能

力。拿X86平台来说,有几种主要的内存屏障:

1. lfence,是一种Load Barrier 读屏障

2. sfence, 是一种Store Barrier 写屏障

3. mfence, 是一种全能型的屏障,具备lfence和sfence的能力

4. Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线和高速

缓存加锁,可以理解为CPU指令级的一种锁。它后面可以跟ADD, ADC, AND, BTC, BTR, BTS,

CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG等指令。

内存屏障有两个能力:

1. 阻止屏障两边的指令重排序

2. 刷新处理器缓存/冲刷处理器缓存

对Load Barrier来说,在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据;对Store Barrier来说,在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存。

Lock前缀实现了类似的能力,它先对总线和缓存加锁,然后执行后面的指令,最后释放锁后会把高速

缓存中的数据刷新回主内存。在Lock锁住总线的时候,其他CPU的读写请求都会被阻塞,直到锁释放。

不同硬件实现内存屏障的方式不同,Java内存模型屏蔽了这种底层硬件平台的差异,由JVM来为不同的

平台生成相应的机器码。

JMM的内存可见性保证

按程序类型,Java程序的内存可见性保证可以分为下列3类:

  • 单线程程序。单线程程序不会出现内存可见性问题。编译器、runtime和处理器会共同确保单线程程序的执行结果与该程序在顺序一致性模型中的执行结果相同。
  • 正确同步的多线程程序。正确同步的多线程程序的执行将具有顺序一致性(程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同)。这是JMM关注的重点,JMM通过限制编译器和处理器的重排序来为程序员提供内存可见性保证。
  • 未同步/未正确同步的多线程程序。JMM为它们提供了最小安全性保障:线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值。未同步程序在JMM中执行时,整体上是无序的,其执行结果无法预知。 JMM不保证未同步程序的执行结果与该程序在顺序一致性模型中的执行结果一致。

未同步程序在JMM中的执行时,整体上是无序的,其执行结果无法预知。未同步程序在两个模型中的

执行特性有如下几个差异。

  1. 顺序一致性模型保证单线程内的操作会按程序的顺序执行,而JMM不保证单线程内的操作会按程序的顺序执行,比如正确同步的多线程程序在临界区内的重排序。
  2. 顺序一致性模型保证所有线程只能看到一致的操作执行顺序,而JMM不保证所有线程能看到一致的操作执行顺序。
  3. 顺序一致性模型保证对所有的内存读/写操作都具有原子性,而JMM不保证对64位的long型和double型变量的写操作具有原子性(32位处理器)。

JVM在32位处理器上运行时,可能会把一个64位long/double型变量的写操作拆分为两个32位的写操作来执行。这两个32位的写操作可能会被分配到不同的总线事务中执行,此时对这个64位变量的写操作将不具有原子性。从JSR-133内存模型开始(即从JDK5开始),仅仅只允许把一个64位long/double型变量的写操作拆分为两个32位的写操

作来执行,任意的读操作在JSR-133中都必须具有原子性

volatile的内存语义

volatile的特性

可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。

原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性(基于这点,我们通过会认为volatile不具备原子性)。volatile仅仅保证对单个volatile变量的读/写具有原性,而锁的互斥执行的特性可以确保对整个临界区代码的执行具有原子性。64位的long型和double型变量,只要它是volatile变量,对该变量的读/写就具有原子性。

有序性:对volatile修饰的变量的读写操作前后加上各种特定的内存屏障来禁止指令重排序来保障有序性。

volatile写-读的内存语义

当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。

当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量。

volatile可见性实现原理

JMM内存交互层面实现

volatile修饰的变量的read、load、use操作和assign、store、write必须是连续的,即修改后必须立即同步回主内存,使用时必须从主内存刷新,由此保证volatile变量操作对多线程的可见性。

硬件层面实现

通过lock前缀指令,会锁定变量缓存行区域并写回主内存,这个操作称为“缓存锁定”,缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据。一个处理器的缓存回写到内存会导致其他处理器的缓存无效。

LOCK前缀指令

是硬件与内存层面进行交互的时候,带有lock前缀的一段机器指令,其作用为

  • 确保后续指令执行的原子性。使用的场景是,在历史的处理器中,lock类型的指令会锁住整个总线,导致其他进程也无法访问到主内存,而inter使用缓存锁定来保证原子性,大大降低了指令的执行开销。
  • 禁止该指令与前面和后面的读写指令重排序。
  • 会等待它之前所有的指令完成、并且所有缓冲的写操作写回内存之后才开始执行,并且根据缓存一致性协议,刷新缓存行的操作会导致其他缓存行中的副本失效。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值