java的内存模型和线程调度

news/2024/9/19 4:06:19 标签: java, 开发语言

硬件的效率与一致性

计算机同时处理多个任务,一方面是因为计算机的运算能力强大,另一方面,也有计算机运算速度和它存储与通信子系统是速度差距太大的原因,很多时间浪费在了IO读取,网络通信等任务上,如果无法并发将会浪费许多的运算资源。

并发执行看起来和充分利用计算机效能有着必然的因果关系,其实不然,他们之间的关系极其复杂,完成一个任务并不只依靠计算机的运算能力,在这个过程中至少还有和内存交互,这个过程的速度和计算机的运算速度相差也是极大的,所以计算机系统不得不处理这个问题,在这里加入多层读写速度尽可能的接近运算速度的高速缓存,在这里作为内存和处理器之间的缓冲:将运算 需要使用的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存 之中,这样处理器就无须等待缓慢的内存读写了

虽然高速缓存解决了处理器和内存速度的问题,但是多个处理器,每一个处理器都有自己的高速缓存,但是他们都共享同一内存,如果多个处理器处理同一块内存上的数据可能导致出现同步问题,为了保证一致性,计算机设置了缓存一致性协议,通过协议保证数据的同步

在这里插入图片描述

Java 内存模型

主内存和工作内存

Java 内存模型的主要目的是定义程序中各种变量的访问规则

这里的变量不止是java中的变量,还包括其他的一下数据如:实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,因为后者是线程私有的

java内存模型规定,所有变量都必须存储到主内存中(硬件主内存中的一部分),每一条线程都有各自的工作内存,线程的工作内存总存储变量的副本,对变量的操作都在工作内存中处理,不能直接操作主内存,同时线程之间的工作内存不允许相互访问,
在这里插入图片描述

内存间交互

一个变量如何从主存拷贝到工作内存,工作内存如何同步回主存这个过程通过8个操作完成(这里long和double另做讨论)

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

这里的操作可以看出,在每次提取和存入数据时都需要进行俩个操作,读取是read和load,存入是stroe和write这里的俩个操作不允许单独发生,而且他们之间必须按顺序执行,但是没有要求连续执行,即他们可以在中间插入其他操作,除了这些要求还有很多其他的要求,这些要求相当繁琐,这些要求保证了并发下的安全

对于 volatile 型变量的特殊规则

volatile关键字定义的变量有俩个特征:

  • 保证此变量对所有线程的可见性 (这里虽然可见但是不保证操作原子性,即写操作不立即同步)
  • 禁止指令重排序优化

关于第一点,volatile修饰的变量虽然是即时可见的,但是对它的写操作不是即时的,写操作不是并发安全的,所以在下面的情况下我们需要通过加锁保证原子性

  • 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
  • 变量不需要与其他的状态变量共同参与不变约束。

关于第二点,首先介绍一下指令重排序优化,编译器和处理器常常会对代码进行重排序优化,以提高程序的执行效率,在执行过程总普通变量仅仅保证所有依赖赋值的地方得到正确的结果,不保证变量的赋值操作与程序中的执行顺序一致。

在单线程中的重排序会判断数据之间的依赖性进行重排序判断,保证其得到的运行结果不出错,但是在并发状态下无法确定的判断线程接的数据依赖关系,这种情况下重排序就可能出现问题。

volatile修饰的变量多执行了操作使其具有内存屏障

  1. Load Barrier(读屏障):在写入一个 volatile 变量后,JVM 会插入一个 Store Barrier(写内存屏障),确保所有之前的写操作都完成之后,再写入 volatile 变量。
  2. Store Barrier(写屏障):在读取一个 volatile 变量前,JVM 会插入一个 Load Barrier(读内存屏障),确保所有之前的读操作都完成后,再读取 volatile 变量。

例子

class VolatileExample {
    private volatile boolean flag = false;
    private int data = 0;

    public void set(int value) {
        data = value; // 写入数据
        flag = true;  // 设置标志
    }

    public int getDataIfReady() {
        if (flag) { // 检查标志
            return data; // 返回数据
        }
        return -1;
    }
}
线程操作
  • 线程 A 负责设置 data 值并更新 flag
  • 线程 B 定期检查 flag 是否为 true,如果是,读取 data
正常情况(没有重排序)
  • 线程 A 执行 data = 10;
  • 线程 A 执行 flag = true;。由于 flagvolatile 变量,这个写入操作不能被重排序到 data = 10; 之前。
  • 线程 B 检查 flag,发现它为 true
  • 线程 B 读取 data,得到 10。
重排序情况(理论上的非法重排序)
  • 线程 A 执行 flag = true;
  • 线程 A 执行 data = 10;。如果编译器或处理器错误地重排序了这两个操作,data 的写入将发生在 flag 的设置之后。
  • 线程 B 检查 flag,发现它为 true,但由于重排序,data 还没有被设置。
  • 线程 B 读取 data,可能得到一个未定义的或旧的值。

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

java虚拟机的内存八种操作都具有原子性,但是对应64位数据类型,虚拟机允许将64位数据的读写操作划分为俩个32位读写操作进行,这就是所谓的“long 和 double 的非原子性协定”

如果有多个线程共享一个并未声明为 volatile 的 long 或 double 类型的变量,并且同 时对它们进行读取和修改操作,那么某些线程可能会读取到一个既不是原值,也不是其 他线程修改值的代表了“半个变量”的数值。但是后面虚拟机加了专门的参数来约束虚拟机对数据类型进行原子性的访问

原子性、可见性与有序性

原子性:一个或者一系列操作在执行过程中,要么全做,要么不做,不可分割

基本操作一般都是具有原子性的

复合操作一般不具有,需要使用同步机制(synchronized)保证原子性

可见性:一个线程修改了共享变量的值,其他线程能够立即看到这个改变的特性

volatile可以确保该变量的读写操作对所有线程都是可见的

有序性:程序执行的顺序与代码中的顺序一致

使用synchronizedvolatile可以确保操作的有序性。

先行发生原则

java内存模型中有序性并不完全依赖volatile 和 synchronized 来完成,,他有一个先行原则保证有序性

依赖这个原则, 我们可以通过几条简单规则一揽子解决并发环境下两个操作之间是否可能存在冲突的所 有问题

先行发生是 Java 内存模型中定义的两项操作之间的偏序关系,比如说操作 A 先行发生于操作 B,其实就是说在发生操作 B 之前,操作 A 产生的影响能被操作B观察到

下面是一些天然先行发生关系,如果俩个操作不满足且无法推导出来,那么虚拟机就可以对他们进行任意重排序

  • 程序次序规则:在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作。注意,这里说的是控制流顺序而不是程序代码 顺序,因为要考虑分支、循环等结构。
  • 管程锁定规则:一个 unlock 操作先行发生于后面对同一个锁 的 lock 操作。这里必须强调的是“同一个锁”,而“后面”是指时间上的先后。
  • volatile 变量规则:对一个 volatile 变量的写操作先行发生于 后面对这个变量的读操作,这里的“后面”同样是指时间上的先后。
  • 线程启动规则:Thread 对象的 start()方法先行发生于此线程的 每一个动作。 、
  • 线程终止规则:线程中的所有操作都先行发生于对此线 程的终止检测,我们可以通过 Thread::join()方法是否结束、Thread::isAlive()的返回值等 手段检测线程是否已经终止执行。
  • 线程中断规则:对线程 interrupt()方法的调用先行发生于 被中断线程的代码检测到中断事件的发生,可以通过 Thread::interrupted()方法检测到是 否有中断发生。
  • 对象终结规则:一个对象的初始化完成(构造函数执行结束)先 行发生于它的 finalize()方法的开始。
  • 传递性:如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那就可以得出操作 A 先行发生于操作 C 的结论。

时间先后顺序与先行发生原则之间基本没 有因果关系,所以我们衡量并发安全问题的时候不要受时间顺序的干扰,一切必须以先 行发生原则为准

java_143">java线程

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

  1. 线程的实现

实现线程主要有三种方式:使用内核线程实现(1:1 实现),使用用户线程实现 (1:N 实现),使用用户线程加轻量级进程混合实现(N:M 实现)。

  1. 内核线程实现

内核线程(KLT)由操作系统内核直接支持,这种线程由内核完成线程切换,内核通过操纵调度器对线程进行调度,并负责将线程的任务映射到各个处理器上,每一个内核线程都可以看成内核的一个分身,这样操作系统就有能力同时处理多个事情,

我们一般不使用内核线程,使用的是轻量级进程(LWP),是内核线程的一种接口,也就是我们通常讲的线程,每一个轻量级线程都有一个内核线程支持

在这里插入图片描述

内核线程的支持使,轻量级线程成为一个个独立的调度单元,即使某一个被阻塞,进程仍然可以完成工作,不过因为是系统调用的所以,他的代价相对较高,每一个轻量级线程都需要内核线程的支持,所以一个系统可以支持的轻量级线程是有限的,

  1. 用户线程实现

广义上来讲,一个线程只要不是内核线程,都可以认为是用户线程**(UT)**的一种。

狭义上的用户线程指的是完全建立在用户空间的线程库上,系统内核不能感知到 用户线程的存在及如何实现的

这种线程不需要切换到内核态,因为操作很快消耗也低,可以支持更大的线程数量,但是因为没有内核线程的支持,所有线程操作都需要用户自己处理,线程的创建、销毁、切换和调度都是用户必 须考虑的问题,而且由于操作系统只把处理器资源分配到进程,很多问题解决起来会相当困难,一般应用程序都不倾向使用用户线程。

  1. 混合实现

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

在这里插入图片描述

  1. Java 线程的实现

java虚拟机中的线程实现,主要是轻量级线程,每一个java线程和操作系统线程中没有额外的空间结构,这种情况下虚拟机没有干涉线程调度,全部交给操作系统处理,

虚拟机设计规范中没有限制,线程的映射,所以不同的平台选择的线程实现

  1. java_184">java线程调度

线程调度是指系统为线程分配处理器使用权的过程,调度主要方式有两种

  1. 协同式线程调度

由线程执行时间由线程自己决定,在本线程执行完切换其他线程,好处在于线程切换对应线程可知,没有什么线程同步的问题,但是问题在于如果由一个线程坚持不让出处理器,那么整个程序就将被阻塞在哪里,很容易导致程序崩溃

  1. 抢占式线程调度

每一个线程由系统来分配执行时间,线程的切换对线程不可知,在中线程可以自己主动让出执行时间,但是主动让出的好处在于线程的执行时间是系统可控的,也不会有一个线程导致整个进程甚至整个系统阻塞的问题。

虽然说 Java 线程调度是系统自动完成的,但是我们仍然可以“建议”操作系统给某些 线程多分配一点执行时间,另外的一些线程则可以少分配一点——这项操作是通过设置 线程优先级来完成的。Java 语言一共设置了 10 个级别的线程优先级,优先级越高越容易被执行

线程优先级不是一个稳定的调节手段,优先级可能会被系统自行改变,

例如在 Windows 系统中存在一个叫“优先级推进器”的功能 ,大致作用是当系统发现一个线程被执行得特 别频繁时,可能会越过线程优先级去为它分配执行时间,从而减少因为线程频繁切换而 带来的性能损耗,所以我们不能通过优先级来判断一组线程来优先执行哪一个

  1. 状态转换

java的线程状态有6种

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

运行(Runnable)正在执行 或者 正在等待操作系统为他分配执行时间

无限期等待(Waiting) 不会被分配时间,需要等待其他线程显示唤醒

限期等待(Timed Waiting) 不会被分配执行时间,但是不需要等待其他线程唤醒,只需要等待一定时间就会被系统唤醒,比如常用的sleep()方法

阻塞(Blocked) 线程被阻塞了,阻塞状态是等待获取一个排他锁,当另一个线程放弃了这个锁,他得到这个锁,阻塞状态结束

结束(Terminated) 已终止线程的线程状态,线程已经结束执行

java_218">java与协程

前面提到过,大部分java虚拟机的线程模式是内核线程支持的轻量级线程,这种映射到操作系统的线程有天然的缺陷,那就是切换、调度成本高昂,系统能容纳的线程数量也很有限。过去处理一个请求会花很长时间在单体应用上,但是现在的业务复杂程度不断上升

传统的 Java Web 服务器的线程池的容量通常在几十个到两百之间,当程序员把数以 百万计的请求往线程池里面灌时,系统即使能处理得过来,但其中的切换损耗也是相当可观的。

现在的核心问题是线程切换的调度成本过高,线程的调度成本主要来自用户态和核心态的切换,而这两种状态转 换的开销主要来自于响应中断、保护和恢复执行现场的成本。

程序不只是有代码,代码执行的时候需要有上下文的数据支持,对于线程来说是存储在方法调用栈中的一个个数据,对应操作系统是存储在内存,缓存中的数据,在发生线程切换的时候,不光是代码的切换,上下文数据也要进行转移,要将这些数据在缓存,寄存器中进行来回的拷贝,这个资源消耗无法忽视

如果采用用户线程可以避免吗,答案是不可以,但是从操作系统交到程序员手上,就可以进行一些操作缩减这些开销

在之前单人单工作业形式下,天生不支持多线程,在这种情况下就已经出现了”栈纠缠“,由用户模拟多线程,自己保护恢复现场的工作模式,其大致的原理是通过在内存里划出一片额外空间来模拟调用栈,只要其他“线 程”中方法压栈、退栈时遵守规则,不破坏这片空间即可

在后来,模拟多线程的操作是少了很多,但是没有完全消失,演化为用户线程继续存在,由于最初多数的用户线程是被设计成协同式调度的,所以他又叫协程,因为他会完整的调用栈的保护,恢复工作,所以也叫”有栈协程“,是为了和后来的”无栈协程“区分

协程的主要优势是轻量,很多支持协程的应用中,同时并存的协程数量可以达到几十万,协程当然也有他的局限,想在应用层实现(调度栈,调度器等)的东西太多,同时协程在很多语言,框架中设计成协同式调度,协同式调度的缺点前面提到过,很可能占着处理器不让出,导致程序崩溃

java_236">java的优化方案

对于有栈协程,有一种特例实现名为纤程,他是一种经典的有栈协程。

OpenJDK 在 2018 年创建了 Loom 项目,在这个项目是java为了解决上面问题的解决方案,这个项目的目的不是取代当前基于操作系统的线程,是为了使俩个并发模型在虚拟机中共存,

新模型有意地保持了与目前线程模型相似 的 API 设计,它们甚至可以拥有一个共同的基类,这样现有的代码就不需要为了使用纤 程而进行过多改动,甚至不需要知道背后采用了哪个并发编程模型

在新并发模型下,一段使用纤程并发的代码会被分为两部分——执行过程 和调度器。执行过程主要用于维护执行现场,保护、恢复 上下文状态,而调度器则负责编排所有要执行的代码的顺序。将调度程序与执行过程分 离的好处是,用户可以选择自行控制其中的一个或者多个,而且 Java 中现有的调度器也 可以被直接重用。


http://www.niftyadmin.cn/n/5664945.html

相关文章

保护您的企业免受网络犯罪分子侵害的四个技巧

在这个日益数字化的时代,小型企业越来越容易受到网络犯罪的威胁。网络犯罪分子不断调整策略,并使用人工智能来推动攻击。随着技术的进步,您的敏感数据面临的风险也在增加。 风险的不断增大意味着,做好基本工作比以往任何时候都更…

linux入门到实操-6 Linux服务管理、系统运行级别、配置服务开机启动和关闭防火墙、关机重启

教程来源:B站视频BV1WY4y1H7d3 3天搞定Linux,1天搞定Shell,清华学神带你通关_哔哩哔哩_bilibili 整理汇总的课程内容笔记和课程资料(包含课程同版本linux系统文件等内容),供大家学习交流下载:…

MacOS安装MAT教程

MAT下载地址MAT下载地址MAT下载地址MAT下载地址 如果不知道你的芯片类型, 可以执行如下命令 uname -m

MySQL系列—11.Redo log

1.简介 概念 redo log用于记录事务操作变化,记录的是数据被修改之后的值,(tbs space id page no action)。 作用 尚未完成的DML,数据库崩溃则用log恢复。保证事务持久性。 ( 1 ) 在页面修改完成之后,脏页刷入磁盘之…

Oracle VM VirtualBox仅主机(Host-0nly)网络实现外网连接

目录 1.仅主机(Host-0nly)网络介绍 1.操作步骤 2.测试​编辑 “如果您在解决类似问题时也遇到了困难,希望我的 经验分享 对您有所帮助。如果您有任何疑问或者想分享您的经历,欢迎在评论区留言,我们可以一起探讨解决方案。祝您在编程路上顺利…

1.pytest基础知识(默认的测试用例的规则以及基础应用)

一、pytest单元测试框架 1)什么是单元测试框架 单元测试是指再软件开发当中,针对软件的最小单位(函数,方法)进行正确性的检查测试。 2)单元测试框架 java:junit和testing python:un…

谷歌浏览器扩展程序怎么提升CSS开发效率

在现代Web开发中,CSS(层叠样式表)是不可或缺的一部分,它负责网页的视觉呈现和布局设计。为了提高CSS开发的效率,谷歌浏览器提供了许多实用的扩展程序。本文将介绍几个关键的扩展程序,并探讨如何利用它们来优…

Ubuntu上如何使用sh文件更新CMake

1. 环境版本 以下版本仅为验证版本,不代表仅在该环境下有效。 项目版本Ubuntu18.04.6 LTS默认CMake3.10.2更新CMake3.14.0 2. 问题描述 Ubuntu 18.04.6 LTS自带的apt包管理器支持CMake最高版本只到3.10.2,无法使用默认apt工具更新CMake。 3. 安装方…