一、多线程为什么会出现安全问题

为什么多线程在执行的时候会造成安全问题呢,下面我们来了解一下所谓的线程安全倒地时怎么来的。

一、内存模型简述

了解的同学都知道java内存模型被分为了五个区域,程序计数器、堆、虚拟机栈、本地方法栈以及方法区,理论上方法区也是属于堆中的一部分,只不过方法区是堆中的一块永久区域,也就是垃圾回收不是很频繁,但绝不是不进行垃圾回收,而堆中的垃圾回收则相对频繁的进行,我们稍微来看一下五个区域的作用,下面上网上的一张分区图。
图片说明

1. 程序计数器:

我们都知道线程之间的执行时并发的,既然是并发的那就存在频繁切换,如果线程A执行一段代码执行到一半,线程B抢到了CPU执行权,当线程B执行完后,如何能够找到线程A被抢断执行到的位置呢,这就是计数器的作用。

也就因此每个线程都对一个程序计数器,且是该线程独享的,也就是私有(若不是私有的则找不到被打断是哪个线程的哪一行代码),而计数器就记录了线程正在执行的内存地址,以确保被打断是能够回到原来的地方再次执行

2.虚拟机栈:

虚拟机栈也叫本地方法栈,在java虚拟机中每一个线程都会对应一个栈,也就是说一个线程在执行时在创建程序计数器的同时会创建一个栈,

每个java虚拟机栈则是由多个栈帧组成的,而每个栈帧则对应了一个方法(通常来说线程里面调用的都是多个方法,那么这里面的每一个方法都对应一个栈帧),栈帧在方法运行时,创建并入栈,方法执行完毕后该栈帧会弹出栈帧中的元素作为返回值,并且栈帧也会被清除,而虚拟机栈中主要存放局部变量和引用型变量,因为是每一个线程都会创建一个栈,所以是私有的。</font>

虚拟机栈中都有什么?

  • Java虚拟机栈(Java Virtual Machine Stacks) 是线程私有的,它的生命周期与线程相同。虚拟机栈为虚拟机执行Java方法

(也就是字节码)服务,虚拟机栈描述的是Java方法执行的内存

  • 模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

    每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程

    经常有人把Java内存区分为堆内存(Heap)和栈内存(Stack),这种方法比较粗糙,Java内存区域的划分实际上远比这复杂。

    这种划分方式的流行只能说明大多数程序员最关注的、与对象内存分配分配关系最密切的内存区域是这两块。其中所指的“堆”

    笔者在后面会专门讲述,而所指的“栈”就是现在讲的虚拟机栈,或者说是虚拟机栈中局部变量表部分

    局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象

    引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄

    或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。

    其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。局部变量表所需的

    内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

    在Java虚拟机规范中,对这个区域规定了两种异常情况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError

  • 异常:如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),

    如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

运行时栈帧结构

  • 栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态链接和方法返操作数栈、动态链接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。

  • 每个栈帧都包括了局部变量表、操作数栈、动态链接、方法返回地址和一些额外的附加信息。在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的Code属性之中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。

  • 一个线程中的方法调用链可能会很长,很多方法都同时处于执行状态。对于执行引擎来说,在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧(Current Stack Frame),与这个栈帧相关联的方法称之为当前方法(Current Method)。执行引擎运行的所有字节码指令都只针对当前栈帧进行操作,

  • 局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序编译为Class文件时,就在方法Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量。

  • 局部变量表的容量以变量槽(Variable,下称Slot)为最小单位,虚拟机规范中并没有明确指明一个Slot应占用的内存空间大小,

  • 只是很有导向性地说到每个Slot都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据,这8种数据类型,都可以使用32位或更小的物理内存来存放,但这种描述与明确指出“每个Slot占用32位长度的内存空间”

3. 方法区:

方法区则是java堆中的永久区域,主要存放了一些类的信息,例如:类的全限定名、修饰符,方法名称,static静态常量(注:指定义就已经赋值)、final类型的常量,类中的字段名称,方法的参数等,因此方法区是所有线程共享的。

常量池: 

常量池是属于方法区的一块内存,又分为运行时常量池和静态常量池,这里就不细说了;

常量池中存储的数据可以分成两部分,一类是是字面量(指:字符串、final常量等)、另一类则是引用量(指:类、接口,方法和字段名称以及描述),常量池则是在编译期间就已经确定被保存在已经编译好的.class文件中

4. 本地方法栈:

本地方法栈和虚拟机栈类似,只不过它是专门用来执行native方法的,而虚拟机栈是执行java的方法的,而native方法通常调用的是java一些最底层的东西,例如C语言等;

5. 堆内存:

 堆内存用于存放由new创建的对象和数组,每个实体都有一个内存地址值,实体中的变量都有默认初始化值,即使实体不在使用了,不把实体的引用指向null,垃圾回收也不会执行回收

二、线程安全问题的分析:

上面我们大致了解了一下内存的分布,那么多线程在运行过程是在哪一块出了问题,而导致输出的结果不正确的,来更具传智播客老师讲课的一张图来分析一下流程:

图片说明

如上如果我们写了一个MoneyDemo类,里面有一个main方法,来看一下它的执行过程会使什么样的:

    第一步:那么类加载器会将MoneyDemo类的.class文件加入到内存中,方法区会存入我们的类的全限定名、修饰关键字、里面的方法名称、参数、返回值等,

    第二步:java的类执行引擎就会去方法区找到我们的MoneyDemo类的信息,找要启动的方法就是main方法,为main方法创建虚拟机栈和程序计数器,同时将new Object()则是存入到堆内存中个,而Object obj 则会被存入栈内;

    第三步:在java虚拟机栈找到main方法的栈帧后,请求CUP资源,那么虚拟机就会将java虚拟机栈当前要执行main方法的信息存入高速缓存寄存器中,CUP获取高速缓存内的信息去执行代码。

    第四步:若这个时候我们Object对象进行更改,更改的信息会先存入高速缓存,然后根据栈帧找到虚拟机栈中的main方法,然后将更改的内容同步不到堆内存中,

    那么这时候问题就出现了,如果有两个线程A和B,线程A正修改了Object对象的内容,还没有来的即同步到主内存,而线程B却在读堆中的Object对象,那么这时候读出来的信息就是错误的数据,至此我们的多线程为什么会出现安全问题也就找到了。

三、结论:

    看到这里想必都明白了,多线程下出现安全问题,就是存在多条线程操作同一数据,而CPU执行时先将主内存中的数据拷贝一份作缓存,之后的操作不在从请求主内存中拿数据而是从缓存中获取数据,如果执行完后有更改再同步回主内存,这时候如果存在多条线程操作,就会出现主内存和缓存中数据的不一致,而导致了最终结果的错误,而单线程情况下则完全不会出现这一情况。

感谢 は問わない 参考链接https://www.cnblogs.com/zouxiangzhongyan/p/11502352.html

感谢 小凡_8c8f 参考链接https://www.jianshu.com/p/484ffd69604b