jvm笔记06:java内存模型和线程

这个章节中,可以学习到,java的内存模型,线程的实现,以及java线程的实现的原理

java内存模型

       java虚拟机规范中试图定义一种java内存模型来屏蔽各种硬件和操作系统的内存访问的差异,以实现让java程序再各个平台下都能达到一致性内存访问效果。

主内存和工作内存

       java线程模型规定所有的变量都存储在主内存,每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中执行,而不能直接操作主内存的变量,不同的线程无法直接访问对方的工作内存中的变量,线程间变量值得传递需要通过主内存来完成。

《jvm笔记06:java内存模型和线程》

内存间交互操作

          java内存模型定义了8种操作,来定义主内存和工作内存之间的交互协议,虚拟机实现时会保证这8种操作都是原子的、不可再分的(对于double和long类型变量来说,read,load,store,write操作在某些平台上允许例外)

lock(锁定):作用于主内存的变量,它把一个变量标志为一条线程独占的状态。
unlock(解锁):作用于主内存中的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎。
assign(赋值):作用于工作内存的变量,它把一个从执行引擎接受到的值赋给工作内存的变量。
store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
write(写入):作用于主内存中的变量,它把store操作从主内存中得到的变量值放入主内存的变量中。

java 内存模型还规定了在执行上诉8种操作时候必须满足如下的规则:

1)不允许 read和load、store和write操作之一单独出现。

2)不允许一个之后线程丢弃它的最近的assign操作,即变量在工作内存中改变了必须把该变化同步到主内存中。

3)不允许一个线程无原因的(没有发生过assign操作)把数据从线程的工作内存同步到主内存。

4)一个新的变量只能再住内存诞生,不允许在工作内存中直接使用一个未被初始化的变量,换句话说,就是对一个变量实施use、store操作之前,必须先执行过了assign和load操作。

5)一个变量在同一时刻只允许一条线程对其惊醒lock操作,但lock操作可以被同一条线程重复执行多次,多次lock,只有执行相同次数的unlock,变量才会被解锁

6)如果对一个变量执行lock操作,那将会清空工作内存中次变量的值,在执行引擎使用这个变量前,需要重新执行load或者assign操作初始化变量的值。

7)如果一个变量事先没有被lock,就不允许它执行unlock操作,也不允许unlock一个被其他线程锁定的变量。

8)对一个变量执行unlock之前,必须把此变量同步回主内存(执行store和write操作)

对于volatile型变量的特殊规则

         当一个变量定义成volatile的时候,他将具备两种特性。第一是保证此变量对所有线程的可见性,这里的可见性是指,当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。第二是使用volatile可以禁止指令重排序,普通的变量仅仅会保证在该方法执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中执行顺序的一致。

         由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然要通过加锁来保证原子性。

1)运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。

2)运算不需要与其他的状态变量共同参与不变约束。

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

        java内存模型要求lock、unlock、read、load、use、assign、store、write这8个操作都具有原子性,但是对于64位的数据类型(long和double),在模型中特别定义了一条相对宽松的规定:允许虚拟机将没有被volatile修饰的64位数据的读写划分为两次32位操作来进行,即允许虚拟机实现跨越不保证(目前商用虚拟机都保证了)64位数据类型的load、store、read、write这四个原子性操作,这点就是所谓的long和double的非原子性协定。

       由于商用的虚拟机保证64位的原子性,所以我们再平时编写代码的时候不需要对long和double变量专门声明volatile。

原子性、可见性与有序性

         java内存模型是围绕着在并发过程中如何处理原子性、可见性、有序性这三个特征来建立的,我们逐个来看一下哪些操作实现了这三个特性。

原子性:我们大致可以认为基本数据类型的访问读写是具备原子性的(例外就是long和double的非原子协定,读者只要知道这件事就可以了,无序太过在意这些几乎不会发生的情况)。如果应用场景需要一个更大范围的原子性保证,java内存模型还提供了lock和unlock操作来满足这种需求,反应到java代码就是synchronized关键字,在synchronized之间的操作也是具有原子性的。

可见性:可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。可以说volatile保证了多线程操作时变量的可见性,普通变量不能保证这一点。除了volatile只有,synchronized和final这两个关键字也可以实现可见性。synchronized的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store和write)”。final的可见性是指被final修饰的关键字一旦初始化完成,其他线程中就能看见final字段的值。

有序性:java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性。volatile关键字本身就包含了禁止指令重排序的语意,而synchronized则是由“一个变量在同一时刻只允许一条线程对齐进行lock操作”这条规则获取的,这条规则决定了持有同一个锁的两个同步快只能串行地进入。

先发性原则

           先行发生是java内存模型中定义的亮相操作之间的偏序关系,如果操作A先行发生于操作B,其实就是说在发生B操作之前,操作A产生的影响能被操作B观察到,“影响”包括修改了内存变量的值、发送了消息、调用了方法等。下面是Java内存模型的一些“天然的”先行发生关系,这些先行发生关系无须任何同步器协助就已经存在,可以在编码中直接使用。如果两个操作之间的关系不在此列,并且无法从下列规则推导出来,他们就没有顺序性保障,虚拟机可以对他们随意地进行重排序

1)程序次序规则:在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确的说,应该是控制流顺序而不是程序代码,需要考虑分支、循环等结构

2)管成锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而“后面”是指时间上的顺序。

3)volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是时间上的先后顺序。

4)线程启动规则:Thread对象start()方法先行发生于此线程的每一个动作。

5)线程终止规则:线程中所有的操作都先行发生于此线程的终止检查,我们可以通过Thread.join()方法结束,Thread.isAlive()的返回值等手段检查到线程已经终止执行。

6)线程中断规则:对线程interrupt()方法调用先行发生于被中断的线程代码检测到中断时间的发生,可以通过Thread.interrupted()方法检查到是否有中断发生。

7)对象终结规则:一个对象的初始化完成先行发生于它的finalize()方法的开始

8)传递性:如果A先行发生于操作B,操作B先行发生于操作C,那么可以得出操作A先行发生于操作C的结论。

java与线程

线程实现

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

使用内核线程实现

       内核线程(Kernel-Level Thread,KLT)就是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操作调度器对线程进行调度,负责将线程的任务映射到各个处理器上。程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口—-轻量级进程(Light Weight Process,LWP),轻量级进程就是我们通常意义上的线程。

         局限性:由于基于内核线程实现,所以各种线程操作如创建、析构及同步都需要进行系统调用,而系统调用的代价比较高,需要再用户态和内核态进行切换;其次每个轻量级进程都需要有一个内核线程的支持,因此轻量级进程需要消耗一定的内核资源,因此一个系统支持轻量级进程的数量是有限的。

《jvm笔记06:java内存模型和线程》

使用用户线程实现

       狭义的用户线程指的是完全建立在用户空间的线程库上的,系统内核不能感知线程存在的实现。用户线程的建立、同步、销毁和调度完全在用户状态完成,不需要内核的帮助,如果实现得当,这种线程不需要切换到内核态,因此操作可以是非常快速且消耗低,也可以支持规模更大的线程数量,部分高性能数据库的多线程就是用用户线程实现的。优势:不需要系统内核支援

劣势:由于没有系统内核支援,需要自己考虑线程的创建、切换、调度,实现复杂,较为困难。

《jvm笔记06:java内存模型和线程》

使用用户线程和轻量级进程混合实现

        这种混合模式下,既存在用户线程,也存在用户轻量级进程。

《jvm笔记06:java内存模型和线程》

java线程实现

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

       在Solaris平台中,操作系统同时支持一对一和多对多的线程模型。通过以下参数设定 –XX:UseLWPSynchroinzed(默认值)和-XX:UseBoundThreads来明确指定虚拟机使用哪种线程模型。

java线程调度

      线程调度是指系统为线程分配处理器使用权的过程,主要调度方式分为两种:协同式线程调度和抢占式线程调度。

      由于协同式线程调度容易发生阻塞导致系统奔溃,所以目前多线程的调度方式多是抢占式调度。

线程转换

       Java语言定义了五有种线程状态,在任意一个时间点,一个线程只能有且只有其中一种状态:

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

2)运行(Runable):Runable包括了操作系统线程状态中的Runing和Ready状态,也就是处于该状态的线程可能在执行,也可能在等待CPU分配执行时间

3)无限等待(Wating):处于这种状态的线程不会被分配CPU执行时间,他们要等待被其他线程显示唤醒,以下方法进入无限期等待状态:

没有设置TimeOut参数的Object.wait() 和没有设置TimeOut参数的Thread.join()方法,LockSupport.park()方法。

4)限期等待(Time Waiting):处于这种状态的也不会分配CPU执行时间,不过无需等待被其他线程显示唤醒,在一定时间之后他们会由系统自动唤醒,以下方法会让线程进入限期等待状态:

Thread.sleep() 

设置了Timeout参数的Object.wait()

设置了Timeout参数的Thread.join()

LockSupport.parkNanos()方法

LockSupport.parkUntil()方法

5)阻塞(Blocked):线程被阻塞了,“阻塞状态”与“等待状态”的区别是:”阻塞状态”在等待获取一个排它锁,这个事件将在另外一个线程放弃这个锁的时候发生;而等待则是在等待一段时间或者唤醒动作的发生。程序等待进入同步区域的时候,线程进进入这种状态。

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

《jvm笔记06:java内存模型和线程》

    原文作者:java内存模型
    原文地址: https://blog.csdn.net/fighterandknight/article/details/55806075
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞