Jvm内存模型与线程

关于Jvm内存模型与线程 主要介绍虚拟机如何实现多线程、多线程之间共享竞争数据导致的问题及解决方案 Fate

关于内存模型

基于高速缓存解决了处理器与内存之间的速度矛盾,但是每个处理器都有自己的告诉缓存,然而共享同一主内存,为了避免各个缓存数据的不一致,牵扯到一个缓存的一致性问题,所以各个处理器访问缓存时都要遵循一些操作协议
内存模型:特定操作协议下,对特定内存或高速缓存进行读写访问

Java内存模型

Java内存模型:用于屏蔽各种硬件和操作系统的内存访问差异

  • 主内存&工作内存
    Java内存模型主要是定义程序中各个变量的访问规则(包括实例变量、静态变量和构成数组对象的元素,但不包括局部变量和方法参数,因为这些是线程私有)
    Java内存模型规定所有变量存储在主内存中(类似前面共享的主内存);每条线程还有自己的工作内存(类似处理器的高速缓存);线程的工作内存中保留了被该线程使用变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行;不同线程无法直接访问对方工作内存中的变量;线程间变量值传递需要通过主内存完成

  • 内存间交互操作
    那么变量是如何从主内存拷贝到工作内存,又是如何从工作内存同步主内存的呢?主要通过8种操作 如图 java内存模型还规定了几个必须满足的规则:
    1.一个变量同一时刻只允许一条线程对其进行lock,但一个线程可以对其Lock多次,然而也有执行相应次数的unlock,变量才能被正确解锁
    2.如果堆变量执行Lock,会清空工作内存中此变量的值,执行引擎使用这个变量前,要重新执行Load,assign操作初始化
    3.如果一个变量没有被lock,那么也不可以被unlock,也不允许unlock一个被其他线程lock的变量 4.在unlock前,需要将变量同步回主内存中

  • 关于volatile变量的特殊规则
    volatile具备两种特性:
    1.保证此变量对所有线程可见(一个线程改了值,另一个线程是立即可知的)
    Volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存
    由于java的运算非原子操作,volatile变量的运算在并发下是不安全的
    eg:
    for(;;){
      something 
      // 如果线程内该值没有被volatile修饰,那么在进入线程时,只会从主内存同步一次这个值
      // 如果线程内该值被volatile修饰,那么每次访问这个变量都会从主内存中重新读取该值
    }
    

    2.禁止指令重排序优化

    线程a
    static boolean flag = false;
    dosomething()...
    flag = true;
    线程b
    while(!flag){
      // sleep()
    }
    dosomething()...
    

    上面如果flag不用volatile修饰,则flag = true可能被提前执行(指令重排序优化)

  • volatile的作用
    volatile同步性能优于锁
    注:线程T,V/W表示两个volatile变量
    1.线程对V的操作必须要满足顺序如下:load -> use ; use -> load;也就是要求在工作内存中,每次使用V前都要先从主内存刷新最新值
    2.线程对V的操作必须满足:assign -> store ; store -> aassign;也就是要求在工作内存中,每次修改V后都必须同步到主内存中

  • long&double特殊规则
    虽然java内存模型要求上述8个操作都具有原子性,但是对于64位数据类型,则规定:
    允许虚拟机将选择不保证没有被volatile修饰的64位数据类型的load,store,read,write的原子性,也就是如果多个线程同时共享一个未声明volatile的long或double,则有可能取到一个既非原值,又不是其他线程修改后的数值

  • 原子性 可见性 有序性
    原子性:由java内存模型保证的原子性包括:load,store,read,write,assign,use;提供了monitorenter和monitorexit隐式的使用者两个操作,反应到java代码便是同步块synchronized
    可见性:当一个线程修改了共享变量的值,其他线程能立刻得知,普通变量和volatile区别是,volatile保证了新值能立即同步到主内存,以及每次使用前都立即从主内存刷新;
    除了volatile,java还有synchronized和final能保证可见性
    synchronized是由对一个变量执行unlock前,会将变量同步回主内存
    final是指被final修饰的字段在构造器中一旦初始化完成,其他线程就能看到final字段的值 有序性:如果在本线程内观察,所有操作都是有序的,如果在一个线程中观察另一个线程,所有操作都是无序的
    java提供了volatile和synchronized关键字保证操作的有序性,synchronzied确保一个变量在同一时刻只能允许一条线程对其进行lock操作,决定持有同一个锁的两个同步块只能串行进入

  • 先行发生原则
    是判断数据是否存在竞争、线程是否安全的主要依据
    什么是先行发生?
    指java内存模型中定义的两个操作之间的偏序关系,如果说操作A先行发生于操作B,就是说操作B前,操作A产生的影响能被操作B观察到
    如果两个操作关系不在以下列,则没有顺序性保障,虚拟机可以对他们随意重排序 如图

Java的线程

  • 线程的实现
    1.使用内核线程
    直接由操作系统内核支持的线程,一般轻量级进程就是我们通常所讲的线程
    2.使用用户线程
    用户线程的简历、同步、销毁和调度在用户态中完成,不需要内核的帮助,java等语言已经放弃使用用户线程了
    3.使用用户线程加轻量级进程

Java线程的实现还是使用一对一的模型实现,即一条java线程映射到一条轻量级进程中

  • java线程调度(CPU的使用权)
    1.协同式
    执行时间由线程本身控制,自己处理完后通知系统切换到另一个线程
    优势:简单,且没有现成同步问题
    劣势:执行时间不可控,可能会导致程序阻塞
    2.抢占式
    执行时间由系统分配(在java中可以通过yield让出执行时间,但是无法获取执行时间)
    优势:执行时间系统可控,不会有一个线程导致进行阻塞
    java可以通过设置优先级,给线程分配时间

  • 状态转换
    java定义了5种线程状态,任意一个时间点,一个线程只能有且只有一种状态 如图 注:wait和block的区别是,阻塞是等待着获取一个排它锁;而等待是等待一段时间或唤醒动作的发生.