JVM--Java内存模型与线程

1.Java内存模型

1.主内存与工作内存

Java内存模型的主要目的是定义程序中各种变量的访问规则.即关注在虚拟机中把变量值存储到内存和从内存中取出变量值这样的底层细节.此处的变量包括了实例字段,静态字段,构成数组对象的元素,但不包括局部变量与方法参数,因为后者私有,不会被共享.

Java内存模型规定了所有的变量都存储在主内存中.每条线程还有自己的工作内存,线程得工作内存中保存了被该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的数据.不同的线程之间也无法直接访问对方工作内存中的变量,线程间的变量值的传递均需要通过主内存来完成.
在这里插入图片描述关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存,如何从工作内存同步到主内存这一类的实现细节,Java内存模型中定义了一下8种操作来完成.Java虚拟机实现时必须要保证下面提及的每一种操作为原子的,不可再分的(double和long变量来说,load,store,read,write在某些平台上允许有例外)

  1. Lock锁定 作用于主内存变量,将变量标志为一条线程所独占
  2. Unlock解锁 作用于主内存变量,将处于锁定的变量释放出来
  3. Read读取 作用于主内存变量,它将一个变量的值从主内存传输到线程的工作内存中
  4. Load载入 作用于工作内存变量,它把从主内存读取的变量值放入工作内存的副本中
  5. Use使用 作用于工作内存变量,将工作内存变量值传递给执行引擎
  6. Assgin赋值 作用于工作内存变量,将执行引擎的值传递给工作内存的变量
  7. Store存储 作用于工作内存变量,它把工作内存变量传递到主内存中
  8. Write写入 作用于主内存变量,把Store操作从工作内存得到的变量值放入主内存变量中
  • 如果要把一个变量的值从主内存复制到工作内存,那么需要执行read load操作
  • 如果要把一个变量的值从工作内存同步回主内存,那么需要执行store write操作

Java内存模型这2个操作必须顺序执行,但不保证连续执行,即在指令之间可以插入其它指令

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

  • 不允许read load store write单独出现,即不允许一个变量读取到工作内存,但没有变量接收的情况

  • 不允许一个线程丢弃它的assign操作,即变量在工作内存改变必须同步回主内存

  • 不允许一个线程无原因(没有发生assgin赋值操作)把数据从线程的工作内存同步会主内存

  • 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用未被初始化的变量

  • 一个变量同一时刻只允许一条线程对其进行Lock锁定,但Lock操作可以被同一线程重复执行

  • 如果对一个变量执行Lock锁定,会清空工作内存中该副本的值,即执行引擎使用该值会重新load assgin操作初始化该值

  • 如果一个变量事先没有被Lock锁定,那就不允许进行Unlock操作,也不允许Unlock其它线程锁定的变量

  • 对一个变量执行Unlock操作,必须先把此变量值同步回主内存(store write操作)

2.对于volatile型变量的特殊规则

  • 关键字volatile可以说是Java虚拟机提供的轻量级的同步机制.
  • 当一个变量被定义成volatitle之后,它将具备两项特征.
  1. 保证此变量对所有线程的可见性.
    由于volatile变量只能保证可见性,在不符合如下规则的运算场景中,我们仍要通过枷锁来保证原子性.
    1. 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值.
    2. 变量不需要与其他的状态变量共同参与不变约束.
  2. 禁止指令重排序优化.普通的变量仅会保证在该方向的执行过程中所有依赖赋值结果的地方都能够获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致.volatile修饰的变量,赋值后,会多执行一个"lock addl $0x0,(%esp)"操作,相当于一个内存屏障.
    1. lock的作用是将本处理器的缓存写入了内存,该写入动作也会引起别的处理器或者别的内核无效化其缓存, 这种操作相当于对缓存中的变量做了一次"store"和"write"操作.所以通过这样一个空操作,可让前面volatile变量的修改对其他处理器立即可见.

    2. Java内存模型对volatile变量定义的特殊规则的定义.

      • 每次使用V前都必须先从主内存刷新最新的值,用于保证能看见其他线程对变量V所做的修改.
      • 每次修改V后都必须立刻同步回主内存中,用保证其他线程可以看到自己对变量V所做的修改.
      • volatile修饰的变量不会被指令重排序优化,从而保证代码的执行顺序与程序的顺序相同.

3.针对long和double型变量的特殊规则

允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行.,这就是"long和double的非原子性协议".
现代的中央处理器一般都包含了专门用于处理浮点数据的浮点运算器,用来专门处理单,双精度的浮点数据,所以很少有非原子性访问.-XX:AlwaysAtomicAccesses :用来约束虚拟机对所有数据类型进行原子性的访问.

4.原子性,可见性,有序性

原子性(Atomicity)
由 Java 内存模型来直接保证的原子性变量操作包括 read、load、assign、use、store 和 write。大致可以认为基本数据类型的操作是原子性的。同时 lock 和 unlock 可以保证更大范围操作的原子性。而 synchronize 同步块操作的原子性是用更高层次的字节码指令 monitorenter 和 monitorexit 来隐式操作的。

可见性(Visibility)
是指当一个线程修改了共享变量的值,其他线程也能够立即得知这个通知。主要操作细节就是修改值后将值同步至主内存(volatile 值使用前都会从主内存刷新),除了 volatile 还有 synchronize 和 final 可以保证可见性。同步块的可见性是由“对一个变量执行 unlock 操作之前,必须先把此变量同步会主内存中( store、write 操作)”这条规则获得。而 final 可见性是指:被 final 修饰的字段在构造器中一旦完成,并且构造器没有把 “this” 的引用传递出去( this 引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象),那在其他线程中就能看见 final 字段的值。

有序性(Ordering)
如果在被线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句指“线程内表现为串行的语义”,后半句是指“指令重排”现象和“工作内存与主内存同步延迟”现象。Java 语言通过 volatile 和 synchronize 两个关键字来保证线程之间操作的有序性。volatile 自身就禁止指令重排,而 synchronize 则是由“一个变量在同一时刻只允许一条线程对其进行 lock 操作”这条规则获得的,这条规则决定了持有同一个锁的两个同步块只能串行的进入。

5.先行发生原则

先行发生原则:它是判断数据是否存在竞争、线程是否安全的主要依据。先行发生是JMM中定义的两项操作之间的偏序关系。
程序次序规则:
在一个线程内, 按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。

  • 管程锁定规则

    一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,这里的“后面”指的是时间上的先后顺序。

  • volatile变量规则

    对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”指的是时间上的先后顺序。

  • 线程启动规则

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

  • 线程终止规则

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

  • 线程中断规则

    对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生(即先中断,后发现被中断),可以通过Thread.interrupted()方法检测到是否有中断发生。

  • 对象终结规则

    一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。

  • 传递性

    若操作A先行发生于操作B,操作B先行发生于操作C,那么就可以得出操作A先行发生于操作C。

以上的规则都无需任何同步手段.

2.Java与线程

1.线程的实现

线程是比进程更轻量级的调度执行单位,线程得引入,可以把一个进程的资源分配和执行调度分开,各个线程即可以共享进程资源,用可以独立调度.
实现线程有三种方式:使用内核线程实现(1:1实现),使用用户线程实现(1:N实现),使用用户线程加轻量级进程混合实现(N:M实现).

  1. 使用内核线程实现

内核线程(KLT,Kernel-Level Thread),直接由操作系统内核(Kernel,即内核)支持的线程。由内核来完成线程切换,内核通过操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。
每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核叫做多线程内核(Multi-Threads Kernel)。

程序一般不会去直接使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程(Light Weight Process,LWP),即通常意义上的线程*。由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。*轻量级进程与内核线程之间1:1关系称为一对一的线程模型。
在这里插入图片描述

优点:

内核线程保证了每个轻量级进程都成为一个独立的调度单元,即使有一个轻量级进程在系统调用中阻塞了,也不会影响整个进程的继续工作。

缺点:

基于内核线程实现,因此各线程操作等需要系统调用,系统调用代价高,需要在用户态和内核态来回切换。
每个轻量级进程都需要一个内核线程的支持,因此轻量级进程要消耗一定的内核资源,如内核线程的栈空间,因此一个系统支持轻量级进程的数量是有限的。

  1. 使用用户线程实现

用户线程(User Thread,UT)指完全建立在用户空间的线程库上,系统内核不能感知线程存在的实现。用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。这种进程与用户线程之间1:N的关系称为一对多的线程模型。
在这里插入图片描述

优点:

这种线程不需要切换内核态,效率非常高且低消耗,也可以支持规模更大的线程数量,部分高性能数据库中的多线程就是由用户线程实现的。

缺点:

由于没有系统内核的支援,所有的线程操作都需要用户程序自己处理。阻塞处理等问题的解决十分困难,甚至不可能完成。所以使用用户线程会非常复杂。

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

使用用户线程和轻量级进程混合实现这种方式,分别使用了用户线程和轻量级进程的优点。在这种混合模式中,用户线程与轻量级进程的数量比是不定的,即为N:M的关系。
许多UNIX系列的操作系统,如Solaris、HP_UX等都提供了N:M的线程模型实现。
mark

优点:

用户线程还是完全建立在用户空间中,因此用户线程的创建、切换、析构等操作依然廉价,并且可以支持大规模的用户线程并发。
使用轻量级进程作为用户线程和内核线程之间的桥梁,这样可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级线程来完成,大大降低了整个进程被完全阻塞的风险。

  1. Java线程的实现
    Java线程的实现并不受Java虚拟机规范的约束.
    HotSpot它是每一个Java线程都是直接映射到一个操作系统原生线程来实现的,而且中间没有额外的间接结构,所以HotSpot自己是不会去干涉线程调度的,全权交给底下的操作系统去处理,所以何时冻结或唤醒线程,该线程分配多少处理器执行时间,该把线程安排给哪个处理器核心去执行等,都是由操作系统完成的,也都是由操作系统全权决定的.

2.Java线程调度

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

1.如果使用协同式调度的多线程系统,线程的执行时间由线程本身来控制,线程把自己的工作执行完成后,要主动通知系统切换到另一个线程上去.

好处是实现简单,而且由于线程要把自己的事情干完后才会进行线程切换,切换操作对线程自己是可知的,所以一般没有什么先行同步的问题.

坏处线程执行时间不可控制,甚至如果一个线程的代码编写有问题,一直不告知系统进行线程切换,那么程序就会一直阻塞在那里.

2.如果使用抢占式调度的多线程系统,那么每个线程将有系统来分配执行时间,线程的切换不由线程本身来决定.

好处是线程的执行时间是系统可控的,也不会有一个线程导致整个进程甚至整个系统的问题,Java使用的线程调度方式就是抢占式调度.

3.状态转换

  • 新建(New):创建后尚未启动的线程处于这种状态.
  • 运行(Runnable):包括操作系统线程状态中的Runnable和Ready,也就是处于此状态的线程有可能正在执行也有可能正在等待着操作系统为它分配执行时间.
  • 无限期等待(Waiting):处于这种状态的线程不会被分配处理器执行时间,他们要等待被其他线程显示唤醒.
    wait(),join(),park()
  • 期限等待(Timed Waiting):处于这种状态的线程也不会被分配处理器执行时间,不过无需等待被其他线程显示唤醒,在一定时间之后他们会由系统自动唤醒.
    sleep(),wait(),join(),parkNanos(),parkUntil()
  • 阻塞(Blocked):线程被阻塞了,"阻塞状态"与"等待状态"的区别是"阻塞状态"在等待着获取到一个排他锁,这个事件将在另外一个线程放弃这个锁的时候发生;而"等待状态"则是在等待一段时间,或者唤醒动作的发生.在程序等待进入同步区域的时候,线程将进入这种状态.
  • 结束(Terminated):已终止线程的线程状态,线程已经结束执行.
    在这里插入图片描述

3.进程 线程 协程 管程 纤程 概念对比理解

进程 线程 协程 管程 纤程 概念对比理解

纤程
在计算机科学中,纤程(英语:Fiber)是一种最轻量化的线程(lightweight threads)。它是一种用户态线程(user thread),让应用程序可以独立决定自己的线程要如何运作。操作系统内核不能看见它,也不会为它进行调度。

就像一般的线程,纤程有自己的定址空间。但是纤程采取合作式多任务(Cooperative multitasking),而线程采取先占式多任务(Pre-emptive multitasking)。应用程序可以在一个线程环境中创建多个纤程,然后手动运行它。纤程不会被自动运行,必须要由应用程序自己指定让它运行,或换到下一个纤程。

跟线程相比,纤程较不需要操作系统的支持。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值