java内存模型与线程《Java虚拟机》要点精炼

Java内存模型

物理计算的内存模型与乱序排序

物理计算机中的并发问题:大多数的计算任务不能只由处理器完成,而需要通过处理器与内存交互,而处理器处理的速度与内存读取的速度之间差了几个数据集,因此引入了高速缓存来作为内存与处理器之间的缓冲。

将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中。
但这样会涉及到一个重要问题:如果多个处理器处理的是同一块内存,就可能导致缓存不一致,进而影响在主内存的存储。
在这里插入图片描述
除了高速缓存外,还引入了乱序优化。为了使得处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执行(Out-Of-Order Execution)优化,最后再进行重组。Java中也有对指令的重排序优化。

java中的内存模型

基本模型

Java内存模型的主要目标是定义程序中各个变量的访问规则。这里的变量不包括局部变量与方法参数,因为它们是被线程所私有的,不涉及共用。这里的变量是指对象实例,静态变量,和构成数组对象的元素。

java的内存模型包括主内存与工作内存,主内存用于存储所有的变量,而工作内存是每个线程独有的,工作内存中储存了该线程所用到的变量的主内存副本拷贝(不会拷贝整个对象,而是拷贝对象中的部分字段),一个线程对变量进行操作(读取或更改)都需要对工作内存进行操作,而不是直接向主内存操作。
在这里插入图片描述

工作内存与主内存之间的交互

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

基本规则:

  1. 不允许主内存向工作内存传输而工作内存不接受(不允许只有read而没有load),也不允许工作内存向主内存存储而主内存不接受(不允许只有store没有write)。
  2. 不允许一个线程舍弃assign操作。(不允许在工作内存赋值之后,而不同步到主内存)
  3. 不允许一个线程无原因地,即没有发生过任何assign操作就把数据从线程的工作内存同步回主内存中。
  4. 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,即对一个变量实施use、store操作之前必须先执行过了load和assign操作。
  5. 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中。
  6. 如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。

volatile

1.保证变量对所有线程都是可见的,当一个线程更改了这个变量的值,那么当前变量的值对于其他线程是立刻可见的。普通变量是无法做到的,因为需要先在工作内存中赋值(assign),然后在发送回主内存修改。(store->write)
2.禁止指令重排序。指令重排序是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处,但要求重排序不会影响结果。例如指令一要求地址a中的值+10,指令二要求地址a中的值*2,而指令三要求地址b中的值+10。这样的话我们可以将指令三放到指令一和指令二之间,因为指令三和指令一指令三之间没有关系,但指令一不能在指令二后面因为(a+10)*2(a*2)+10的结果不同。

而volatile的思想是设置了内存屏障,不允许将内存屏障后面的指令排序到内存屏障之前,只有一个线程时不需要内存屏障,多个线程才会需要,因为单一线程的字节码是串行执行的。

原子性

原子性:原子性就是指对数据的操作是一个独立的、不可分割的整体。也就是说当数据进行一个操作时,不要切换线程,要在执行结束后才可以切换线程。

以图中为例,我们要想执行i++这个操作, 首先就要i=i+1,之后存储i=2,不能再这个过程中切换到其他线程。
在这里插入图片描述
一般解决互斥性和原子性的方式是Synchronized方法,或Lock方法。

Synchronized方法通过monitorenter和monitorexit来隐式的操作lock和unlock操作。

可见性

可见性是指当一个线程更改了变量的值后,其他线程能立刻得知该变量的值更改。java内存中是通过变量修改后再同步到主内存,在变量读取前从主内存中刷新这种依赖主内存作为媒介的方式来保证的可见性。

除volatile外,java中还有final和synchronized可以实现可见性

同步块的可见性是当我们assign赋值后,必须先store并write进主内存后才可以调用unlock方法。

而被final修饰的变量,只要在初始化结束后,且没有this赋值逃逸,就可以被所有线程可见。

有序性

线程内字节码以串行执行,线程外禁止指令重排序以保证有序性。

volatile是使用内存屏障组织指令重排序,而synchronized是同一时刻只允许一个线程对对象进行lock操作。

以单例模式为例,new Singleton()其实是三步:(1)为对象分配内存;(2)执行构造函数,初始化成员变量(3)将对象指向分配的内存(此时instance就不是null了),但jvm中允许乱序执行,有可能出现(1)(3)(2)的顺序,那么假如A线程执行了(1)(3),而此时B线程调用getInstance就会返回一个instance对象,但调用上就会出错。
Java 中也可通过Synchronized或Volatile来保证顺序性。

java与线程

实现线程的方式

1.使用内核线程实现

内核线程(KLT)就是直接由系统内核操作的线程,这种线程由内核完成切换,通过调度器(Scheduler)调度对线程进行调度,并负责将线程的任务分配给各个CPU去执行。

而实际中我们不会直接使用内核线程,而是使用轻量级进程(LWP),一个轻量级进程要对应一个内核线程。轻量级进程与内核线程是1:1的对应关系。
在这里插入图片描述
优点:每个轻量级进程都是一个独立的调度单元,即使一个进程被堵塞了,也不影响其他进程正常工作。
缺点:需要在内核态与用户态来回切换。同时每个轻量级进程需要内核线程的支持,因此一个系统所能有的轻量级线程是有限的。

2.使用用户线程实现

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

优点:由于用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助,甚至可以不需要切换到内核态,所以操作非常快速且低消耗的,且可以支持规模更大的线程数量。

缺点:由于没有系统内核的支援,所有的线程操作都需要用户程序自己处理,线程的创建、切换和调度都是需要考虑的问题,实现较复杂。

一对多的线程模型进程:进程与用户线程之间1:N的关系,如图所示
在这里插入图片描述

java线程调度

协同式线程调度线程的执行时间由自己决定,当线程执行结束后会主动通知系统切换线程。只有当前执行线程执行结束后才会继续执行下一个线程。这样做的后果是可能由于错误操作导致其他线程一直处于阻塞状态。

抢占式线程调度:每个线程的执行时间由系统决定,线程执行时间是系统可控的,不存在一个线程导致整个进程阻塞的问题。可以通过设置线程优先级,优先级越高的线程越容易被系统选择执行。

线程间的协作(wait/notify/sleep/yield/join)

  1. 新建(New):线程创建后尚未启动(没有调用start方法)
  2. 运行(Runable):包括正在执行(Running)和等待着CPU为它分配执行时间(Ready)两种
    3.** 无限期等待(Waiting)**:该线程不会被分配CPU执行时间,要通过notify和notifyAll来通知停止等待。以下方法会 让线程陷入无限期等待状态:
    • 没有设置Timeout参数的Object.wait()
    • 没有设置Timeout参数的Thread.join()
    • LockSupport.park()
  3. 限期等待(Timed Waiting):该线程不会被分配CPU执行时间,但在一定时间后会被系统自动唤醒。以下方法会让线程进入限期等待状态:
    • Thread.sleep()
    • 设置了Timeout参数的Object.wait()
    • 设置了Timeout参数的Thread.join()
    • LockSupport.parkNanos()
    • LockSupport.parkUntil()
  4. 阻塞(Blocked):线程被阻塞。和等待状态不同的是,阻塞状态表示在等待获取到一个排他锁,在另外一个线程放弃这个锁的时候发生;而等待状态表示在等待一段时间或者唤醒动作的发生,在程序等待进入同步区域的时候发生。
  5. 结束(Terminated)线程已经结束执行
    在这里插入图片描述

wait

wait方法的作用就是阻塞当前线程等待notify/notifyAll方法的唤醒,或等待超时后自动唤醒。调用wait方法后,线程是会释放对monitor对象的所有权的

notify

既然wait方式是通过对象的monitor对象来实现的,所以只要在同一对象上去调用notify/notifyAll方法,就可以唤醒对应对象monitor上等待的线程了。notify和notifyAll的区别在于前者只能唤醒monitor上的一个线程,对其他线程没有影响,而notifyAll则唤醒所有的线程。

sleep

这个结果的区别很明显,通过sleep方法实现的暂停,程序是顺序进入同步块的,只有当上一个线程执行完成的时候,下一个线程才能进入同步方法,sleep暂停期间一直持有monitor对象锁,其他线程是不能进入的,而wait方法则不同,当调用wait方法后,当前线程会释放持有的monitor对象锁,因此,其他线程还可以进入到同步方法,线程被唤醒后,需要竞争锁,获取到锁之后再继续执行。

java线程安全

Syncronized

Synchronized的使用原理:每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

monitorenter
1.如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2.如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

monitorexit
执行monitorexit的线程必须是object所对应的monitor的所有者。
指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

对于普通同步方法,锁是当前实例对象。因此在类Test中对两个方法进行上锁,同时在另一个类中创建一个Test实例,并创建两个线程分别执行Test实例的A、B两个方法,两个线程会按顺序执行。线程2需要等待线程1执行完才能执行。

**对于静态同步方法,锁是当前类的Class对象。**当我们在类Test中创建了两个静态方法,同时对静态方法上锁。在另一个类中即使创建两个对象,并创建两个线程分别执行对象1的A方法和对象2的B方法,线程2也会等线程1结束之后才能执行。

对于同步方法块,锁是Synchonized括号里配置的对象。对于代码块的同步实质上需要获取Synchronized关键字后面括号中对象的monitor,由于这段代码中括号的内容都是this,而method1和method2又是通过同一的对象去调用的,所以进入同步块之前需要去竞争同一个对象上的锁,因此只能顺序执行同步块。

Lock接口

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章