【JVM】第十二章 Java内存模型与线程

本文详细介绍了Java内存模型(JMM),包括主内存与工作内存、内存间交互操作,以及原子性、可见性和有序性。同时,讨论了硬件层面的处理器、高速缓存和主内存的关系,以及多线程环境下指令重排序可能带来的问题。文章还概述了Java线程的实现、调度和状态转换,强调了线程并发的挑战与解决方案。
摘要由CSDN通过智能技术生成

第十二章 Java内存模型与线程

高效并发:多任务同时处理

衡量一个服务性能高低好坏,每秒事务处理数(TPS)是最重要的指标之一,它代表着一秒内服务端平均能响应的请求总数,而TPS值与并发能力又有非常密切关系。

下面我们来介绍虚拟机是如何来实现、多线程之间由于共享和竞争数据而导致的一系统问题及解决方案。

 

一、硬件的效率与一致性

处理器、高速缓存、主内存

1)处理器与主内存的速度矛盾:处理器与内存交互进行读取运算数据、存储运算结果。这中间有个I/O消耗,由于内存硬件的运算速度和处理器有几个数量级的差距,所以导致内存在处理的时候CPU只能等待。

2)高速缓存:在处理器与主内存之间的缓冲,高速缓存的处理速度和处理器接近,在运算时将数据读到高速缓存中,得到的结果再从缓存同步回主内存。这样处理器就无需等待主内存的读写了。

3)缓存一致性协议:在多处理器环境中,每个处理器都有高速缓存,大家共享主内存,需要制定一引些读写协议,大家都要遵守。

4)乱序执行优化:为了充分利用处理器的每一个单元,处理器可能会对输入的代码进行乱序执行优化,但保证结果与顺序执行的结果一致。

 

二、java内存模型(JMM)

1、主内存与工作内存

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

此处的变量指:字例字段(全局变量)、静态字段、构成数组对象的元素。但不包括局部变量和方法参数。

主内存:存储变量的地方

工作内存:每条线程都有属于自己的工作内存,工作内存中存储的是该线程使用到的变量的主内存副本拷贝。

java线程对变量的所有读写操作都必须在工作内存中操作,不能直接访问主内存。

 

主内存和工作内存也不是特别的方便去对应我们第二章的java运行时数据区。不过大家可以大体的理解一下,主内存存储变量,我们的堆空间是存放对象、方法区存放静态字段

工作内存与java线程相关,而我们的线程是在操作机栈中。所以工作内存是栈空间的一部分。

 

2、内存间交互操作

主内存与工作内存之间的数据交互。JMM中定义了8种操作。我们来看下具体哪8种:

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

 

一个完整的操作流程:

lock锁定对象---》read读取到工作内存中 --》load将变量值存入工作内存变量副本  ——>use 把工作内存的变量传给执行引擎——>assign把执行引擎的结果保存到工作内存中——>store将工作内存的变量传送到主内存中——>write主内存进行写入操作。——>unlock解锁

 

3、原子性、可见性与有序性

java内存模型是围绕并发编程中原子性、可见性、有序性这三个特征建立的。

1)原子性:

一个操作不能被打断,要么全部执行,要么不执行。类似于事务,要么全部执行成功,要么退回原来的状态。

由jmm来直接保障的原子性操作包括:read\load\assign\use\store\write(对单个变量)

如果要保证更大范围的原子性,JMM中提供了lock和unlock。对应的.class中的指令是monitorenter\monitorexit。对应的java语言代码就是synchronized

 

2)可见性:

指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。

这也是有实效性的,对于普通的变量并不能保证马上从工作内存刷新到主内存中。但有几个是特殊的。

 

volatile、synchronized和final。接下来说下这三个是怎么保证实时可见的。

  • volatile:  

(1)当volatile修饰的值发生了改变,能够实时的从工作内存刷新到主内存。

(2) 每次使用当volatile修饰的值时必须从主内存里重新加载到工作内存。

(3)它有也漏洞,只能保障实时可见性,不能保障多线程并发时数据的准备性。如同时读取到一个值,进行计算后再刷新到主内存,可能会出现擦除效果ABA

  • synchronized:

如果对一个变量进行synchronized操作,那么这个变量在执行unlock操作这前,必须先把些变量的值同步回主内存中。也就是加了锁,别的线程不能用。(读写锁,读都不行)

 

  • final:这个不用说大家都知道final修饰的变量的值不能进行修改,没有可见性的问题。

 

3)有序性:

有一句话“如果在线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。”

前半句说明,在线程内部是串行的,并不影响操作执行的结果

后半名是指“指令重排序”现象和“工作内存与主内存同步延迟”问题

java语言提供了volalite和synchronized两个关键字来保证线程之间操作的有序性。

  • volatile关键字本身就包含了禁止重排序的语义。它是依靠硬件支持的内存屏障指令来完成的。

    重排序时不能把最后的指令不能重排序到内存屏障之前的位置。

 

  • synchronized则由一个变量在同一时刻只请允许一条线程对其进行lock操作。也就是说同一个锁的两个同步块只能串行的进入。不存在同步块内如果发生重排,影响另一个线程的问题。

 

三、指令重排序

重排序的目的是提高运行并发度,发生在编译器和处理器阶段,遵循as-if-serial语义(不管怎么重排序,单线程程序的执行结果不能改变),也就是重排序所带来的问题是针对多线程的。

 

在线程A中有两条语句对这两个共享变量进行赋值操作:

a = 1;

b = 2;

 

假设当线程A对a进行复制操作的时候发现这个变量在主内存已经被其它的线程加了访问锁,那么此时线程A怎么办?等待释放锁?不,等待太浪费时间了,它会去尝试进行b的赋值操作,b这时候没被人占用,因此就会先为b赋值,再去为a赋值,那么执行的顺序就变成了:

b = 2;

a = 1;

 

对于在同一个线程内,这样的改变是不会对逻辑产生影响的,但是在多线程的情况下指令重排序会带来问题,看下面这个情景:

在线程A中:

context = loadContext();

inited = true;

在线程B中:

while(!inited ){

    sleep

}

doSomethingwithconfig(context);

 

假设A中发生了重排序:

inited = true;

context = loadContext();

 

那么B中很可能就会拿到一个尚未初始化或尚未初始化完成的context,从而引发程序错误。

 

四、Java与线程

线程是比进程更轻量级的调度执行单位。线程的引入,可以把一个进程的资源分配和执行调度分开。各线程可以共享进程的资源(内存地址、文件I/O等),又可以独立调用(线程是CPU调度的基本单位)

 

1、线程的实现

在java语言中,java.lang.Thread类的实例代表了一个线程。但Thread类的所有关键方法都是声明为Native的,说明线程的实现是依靠平台硬件操作系统来实现的。

这是一个使用内核线程的实现方式。我们来说一下每一部分的作用。

1)内核线程:

KLT就是直接由操作系统内核支持的线程,

这种线程由内核来完成线程的切换。

内核操纵调度器对线程进行调度,并负责将线程的任务映射到各有CPU上

每个内核线程可以视为内核的一个分身。

 

2)轻量级进程(LWP)

程序一般不会去使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程(LWP)。这个轻量级进程就是我们通常意义上所讲的线程。轻量级进程:内核线程 = 1:1

 

轻量级进程的局限性:

  • 由于是基于内核线程实现的,所以各线程操作,如创建、析构及同步都需要系统进行调度。需要用户态和内核态来回的切换
  • 每个轻量级进程都需要一个内核线程支持,需要消耗一定的内核空间,因此一个系统支持的轻量级进程的数量是有限的。

 

2、java线程调度

线程调度是指系统为线程分配处理器使用权的过程。

1)协同线程调度

线程的执行时间由线程本身来控制,线程把自己的工作执行完之后,要主动通知系统切找到另一个线程上。

2)抢占式线程调度

每个线程将由系统来分配执行时间,线程切换不由线程本身来决定。java使用的线程调度就是抢占式调度。

 

3、状态转换

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

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

2)  运行(Runable):处在些状态的线程有可能正在执行,也有可能正在等待CPU分配执行时间。

3)  无限等待(Waiting):等待其它线程显示的唤醒。

4)限期等待(Timed Waiting):在一定时间之后会由系统自动唤醒。

5)阻塞(Blocked): 线程被阻塞了,在等待着获取到一个排他锁,

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值