深入理解JVM笔记-第12章

来源:互联网 发布:sql server 字符串主键 编辑:程序博客网 时间:2024/05/18 20:07

现代处理器大多具有多核:

Java内存模型

Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量(Variables)与Java编程中所说的变量有所区别,它包括了实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,自然就不会存在竞争问题。

主内存与工作存之间具体的交互协议,有8个操作,这些操作都是原子的(非原子性协定:对于64位的数据类型double和float,JMM允许非原子):lock,unlock,read,load,use,assign,store,write。其中lock作用于主内存的变量,它把一个变量标识为一条线程独占的状态。unlock作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。read作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内,存中,以便随后的load动作使用。load作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。use作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。assign作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。store作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。write作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。对于这些操作还有一系列的执行上的规则,这里只记这三条:(1)如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。(2)对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。(3)一个变量在同一时刻只允许一条线程对其进行lock操作

原子性,可见性,有序性:原子性:JMM保证read,load,assign,use,store和write是原子操作,lock和unlock之间的操作原子性,反映到字节码层次就是monitorenter和monitorexit指令,再上就是synchornized可见性:volatile可保证变量可见性,synchornized也可(追溯到JMM就是unlock的那条规则:对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。),还有final,它是指:被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把“this”的引用传递出去(this引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象),那在其他线程中就能看见final字段的值。有序性:volatile的禁止重排序,synchornized(由“一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则得到)

volatile

volatile变量在各个CPU的缓存中是一致的。实现原理(cpu层次):有volatile变量修饰的共享变量进行写操作时会在后面多出一行lock addl $0x0,(%esp)汇编代码,Lock前缀的指令在多核处理器下会引发两件事:1.volatile变量所在处理器缓存行锁住(称为缓存锁定)并写回到系统内存2.这个写回内存的操作会使其他CPU里缓存了该内存地址的数据无效(处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致。,如果通过嗅探一个处理器来检测其他处理器打算写内存地址,而这个地址当前处于共享状态,那么正在嗅探的处理器将使它的缓存行无效,在下次访问相同内存地址时,强制执行缓存行填充。)lock 前缀指令相当于内存屏障,是因为他可以禁止指令重排序,更具体地说就是CPU保证在执行这个lock指令时他前面的指令一定已经执行过了,你可能奇怪,难道一个指令前面的指令不本应当在该指令前面执行吗。其实CPU执行一堆指令并不一定按顺序,只要保证单处理器(注意是单处理器)产生的结果不变CPU是可以对指令重排序后执行的。那为什么lock可以起到内存屏障的作用呢,因为前面也说了重排序的前提是结果不变,lock指令把修改同步到内存,意味着之前的操作都已经执行完了,否则可能产生不同结果。volatile的使用优化:LinkedTransferQueue中的将其共享变量head和tail用了追加到了64字节。为什么?首先要知道很多处理器的缓存行是64个字节宽的,所以如果head和tail都不足64字节的话处理器会将它们都读到同一个缓存行中,在多处理器下每个处理器都会缓存同样的头尾节点,当一个处理器试图修改头节点时,会将整个缓存行锁定,那么在缓存一致性机制的作用下,其他处理器不能访问自己高速缓存中的尾节点,而队列的入队和出队操作则需要不停修改头节点和尾节点,所以在多处理器的情况下将会严重影响到队列的入队和出队效率。如果缓存行非64字节或者共享变量不会被频繁地写则不用这种优化效果不大。

hapen-before原则

hapen-before原则是JMM中关于有序性的原则,是判断数据是否存在竞争、线程是否安全的主要依据。hapen-before是JMM中定义的两项操作之间(深入理解JVM中是这样写的,但我想理解成Java操作在结果上应该也不会有问题)的偏序关系,如果说A hapen-before B就是说在发生B之前,A的影响能被B观察到(如果B**不需要**观察到,那不管A是否执行过都算能被观察到)。JMM保证这些hapen-before原则:1.程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。(这一点还保证了某个层级(编译器(源代码-->字节码)??OR JVM(运行时字节码-->cpu)???)的 As-If-Serial语义)2.管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而“后面”是指时间上的先后顺序。3.volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后顺序。线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。4.线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。5.线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。5.对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。6.传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。

Java线程

实现线程主要有三种:1.使用内核线程实现,不过程序一般不去直接使用内核线程,而是使用内核线程的一种高级接口-—轻量级进程,轻量级进程就是我们通常意义上所讲的线程,每个轻量级进程都由一个内核线程支持。2.使用用户线程实现:的用户线程指的是完全建立在用户空间的线程库上,**系统内核不能感知线程存在**3.使用用户线程加轻量级进程混合实现(Java线程的实现方式):用户线程还是完全建立在用户空间中,因此用户线程的创建、切换、析构等操作依然廉价,并且可以支持大规模的用户线程并发。操作系统提供支持的轻量级进程则作为用户线程和内核线程之间的桥梁,这样可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级线程来完成,大大降低了整个进程被完全阻塞的风险。线程状态变换:

参考资料

深入理解JVMjava并发编程的艺术

java中volatile关键字的含义

0 0