• 【深入理解JAVA虚拟机】第5部分.高效并发.1.Java内存模型与线程。


    1、概述

    摩尔定律:描述处理器晶体管数量与运行效率之间的发展关系。
    Amdahl定律:通过系统中并行化与串行化的比重来描述多处理器系统能获得的运算加速能力。

    从摩尔定律到Amdahl定律的转变,代表了近年来硬件发展从追求处理器频率到追求多核心并行处理的发展过程。

    并发的好处:

    1、计算机的运算速度与它的存储和通信子系统速度的差距太大,充分利用磁盘I/0、网络通信、数据库访问等的等待时间。
    2、一个服务端同时服务多个客户端,已提升TPS。

    本章主要讲解如何在安全的基础上,高效的并发。

    2、硬件的效率与一致性

    问题:处理速度大于内存读写速度。

    解决方法:1、为每个处理器引入高速缓存;2、乱序执行。

    带来新问题:1、缓存一致性;2、有序性

    3、Java内存模型

    Java虚拟机规范中试图定义一种Java内存模型[1](Java Memory Model,JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。

    3.1 主内存与工作内存

    Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存从内存中取出变量这样的底层细节。

    变量(Variables)包括了实例字段、 静态字段和构成数组对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,自然就不会存在竞争问题。(不会放在主内存)

    Java内存模型规定了所有的变量都存储在主内存(Main Memory,对比物理机的主内存)中,每条线程还有自己的工作内存(Working Memory,对比物理机的高速缓存)。

    线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、 赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量[5]。

    不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,

    主内存就直接对应于物理硬件的内存,而为了获取更好的运行速度,虚拟机可能会让工作内存优先存储于寄存器和高速缓存中,因为程序运行时主要访问读写的是工作内存。

    线程、 主内存、 工作内存三者的交互关系如图12-2所示。

    3.2 内存间交互操作

    Java内存模型中定义了以下8种原子操作: 

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

    Java内存模型还规定了在执行上述8种基本操作时必须满足如下规则:

    不允许read和load、 store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况出现。
    不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
    不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化。
    (load或assign)的变量,换句话说,就是对一个变量实施use、 store操作之前,必须先执行过了assign和load操作。
    一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
    如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
    如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定住的变量。
    对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、 write操作)。

    这8种内存访问操作以及上述规则限定,再加上稍后介绍的对volatile的一些特殊规定,就已经完全确定了Java程序中哪些内存访问操作在并发下是安全的。

    由于这种定义相当严谨但又十分烦琐,实践起来很麻烦,所以在12.3.6节中笔者将介绍这种定义的一个等效判断原则——先行发生原则,用来确定一个访问在并发环境下是否安全。

    对于long和double型变量的特殊规则

    64位数字的操作分为两个32位的操作,java内存原子操作不保证这是原子的,但虚拟机实现成原子的了,所以不相关。

    3.3 总结:原子性、 可见性与有序性

    Java内存模型是围绕着在并发过程中如何处理原子性、 可见性和有序性这3个特征来建立的。

    原子性(Atomicity)

    Java内存模型保证了单个操作是原子的,但组合操作,是无法保证的,此时就需要synchronized来保证原子性。

    比如遍历一个vector,虽然vector是安全的,但遍历就不安全了。

    可见性(Visibility)

    可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
    Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的。

    可见性的实现:
    1、volatile

      volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新,保证各了其可见性。

      注意:volatile解决的是一致性问题,但由于Java运算不是原子的,所以,用了volatile的并发操作,仍然是不安全的。

    2、synchronized

      synchronized入口 -> monitorenter -> lock (规则中,lock前,需要读主内存)

      synchronized出口 -> monitorexit -> unlock (规则中,lock前,需要写主内存)

    3、final 常量不会修改,故始终可见。

    有序性(Ordering)

    影响有序性的因素:指令重排序、工作内存与主内存同步延迟。

    普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。

    保证有序的两个方法:

    1、volatile:有volatile修饰的变量,赋值后,多执行了一个空的lock操作,来确保前面的指令不会被重排到后方。

    2、synchronized:通过锁互斥的方式实现同步。

    synchronized关键字可以满足全部3个特性,所以很万能,结果早就了synchronized关键字的滥用,从而影响性能,所以性能优化的很大一块,就是关于synchronized关键字的。

    3.4 先行发生原则

    先行发生是Java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包括修改了内存中共享变量的值、 发送了消息、 调用了方法等。

    “天然的”先行发生关系:

    程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序。
    管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。 
    volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读操作。
    线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。
    线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测。
    线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
    对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
    传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那么操作A先行发生于操作C。

    当且仅当上述规则存在,才无须任何同步器协助就已经存在先行发生关系,否则就没有顺序性保障。

    4、Java与线程

    4.1 线程的实现

    实现线程主要有3种方式:使用内核线程实现、 使用用户线程实现、使用用户线程加轻量级进程混合实现。

    1.使用内核线程实现

    内核线程(Kernel-Level Thread,KLT)就是直接由操作系统内核(Kernel,下称内核)支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。

    每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核就叫做多线程内核(MultiThreads Kernel)。

    程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程(Light Weight Process,LWP),轻量级进程就是我们通常意义上所讲的线程,

    由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。 这种轻量级进程与内核线程之间1:1的关系称为一对一的线程模型。

    2.使用用户线程实现

    用户线程指的是完全建立在用户空间的线程库上,系统内核不能感知线程存在的实现。

    用户线程的建立、 同步、 销毁和调度完全在用户态中完成,不需要内核的帮助。

    如果程序实现得当,这种线程不需要切换到内核态,因此操作可以是非常快速且低消耗的,也可以支持规模更大的线程数量,部分高性能数据库中的多线程就是由用户线程实现的。

    使用用户线程的优势在于不需要系统内核支援,劣势也在于没有系统内核的支援,所有的线程操作都需要用户程序自己处理。

    现在使用用户线程的程序越来越少了,Java、Ruby等语言都曾经使用过用户线程,最终又都放弃使用它。

    3.使用用户线程加轻量级进程混合实现

    使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级线程来完成,大大降低了整个进程被完全阻塞的风险。

    对于Sun JDK来说,它的Windows版与Linux版都是使用一对一的线程模型实现的,一条Java线程就映射到一条轻量级进程之中,因为Windows和Linux系统提供的线程模型就是一对一的。

    4.2 Java线程调度

    线程调度是指系统为线程分配处理器使用权的过程,主要调度方式有两种:

    分别是协同式线程调度(Cooperative Threads-Scheduling)

    协同式调度的多线程系统,线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,要主动通知系统切换到另外一个线程上。

    优点:没有线程同步的问题;缺点:线程执行时间不可控制。

    抢占式线程调度(Preemptive ThreadsScheduling)

    每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定(在Java中,Thread.yield()可以让出执行时间,但是要获取执行时间的话,线程本身是没有什么办法的)。

    在这种实现线程调度的方式下,线程的执行时间是系统可控的,也不会有一个线程导致整个进程阻塞的问题。

    Java使用的线程调度方式就是抢占式调度[1]。

    4.3 状态转换

    Java语言定义了5种线程状态:

    新建(New):创建后尚未启动的线程处于这种状态。

    运行(Runable):Runable包括了操作系统线程状态中的Running和Ready,也就是处于此状态的线程有可能正在执行,也有可能正在等待着CPU为它分配执行时间。

    无限期等待(Waiting):处于这种状态的线程不会被分配CPU执行时间,它们要等待被其他线程显式地唤醒。 以下方法会让线程陷入无限期的等待状态:
    ●没有设置Timeout参数的Object.wait()方法。
    ●没有设置Timeout参数的Thread.join()方法。
    ●LockSupport.park()方法。

    限期等待(Timed Waiting):处于这种状态的线程也不会被分配CPU执行时间,不过无须等待被其他线程显式地唤醒,在一定时间之后它们会由系统自动唤醒。 以下方法会让线程进入限期等待状态:
    ●Thread.sleep()方法。
    ●设置了Timeout参数的Object.wait()方法。
    ●设置了Timeout参数的Thread.join()方法。
    ●LockSupport.parkNanos()方法。
    ●LockSupport.parkUntil()方法。

    阻塞(Blocked):线程被阻塞了。

    “阻塞状态”与“等待状态”的区别是:

    “阻塞状态”在等待着获取到一个排他锁,这个事件将在另外一个线程放弃这个锁的时候发生;

    而“等待状态”则是在等待一段时间,或者唤醒动作的发生。 在程序等待进入同步区域的时候,线程将进入这种状态。

    结束(Terminated):已终止线程的线程状态,线程已经结束执行。

  • 相关阅读:
    ASP.NET 学习日志
    igoogle 小工具
    nios ann 语音识别
    ASP 3.5 读书笔记
    C# delegate and event 续
    paper
    网站白痴的 ASP.NET website 学习日志
    盒子模型
    将对象序列化成json
    不错的Oracle 存储过程例子
  • 原文地址:https://www.cnblogs.com/aoyihuashao/p/10371314.html
Copyright © 2020-2023  润新知