Java多线程的底层原理

想要理解Java的多线程原理,那么不可避免的我们需要学会Java运行时数据区域和Java的类加载机制了

Java运行时数据区域

Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途,以及创建和销毁的时间。
在这里插入图片描述
简单介绍一下什么时线程共享和线程私有
1)线程共享:公共的区域,里面存储的数据对所有的线程都是共享的,谁都可以访问
2)线程私有:每个线程自己独享的区域,每个线程的私有区域都是相互独立的,互不影响
线程共享
1)Java堆(heap):Java虚拟机管理的最大的一块内存,在虚拟机启动时创建,堆这个区域的唯一目的就是用来存放对象实例和数组。
2)方法区:存放已经被JVM加载的类信息、常量、静态变量,即时编译器编译后的代码等数据。
线程私有
1)虚拟机栈:由一个个栈帧组成,每一个栈帧对应一个Java方法执行的内存模型,也就是说每个方法执行时都会创建一个栈帧,用来存放局部变量表,方法返回地址,操作数等信息
在这里插入图片描述
每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程,当然,出栈的顺序自然是遵守栈的后进先出原则的。
2)本地方法栈:存放的信息和虚拟机栈基本一样,区别在于虚拟机栈为JVM执行Java方法服务,而本地方法栈为JVM 用到的Native方法服务
( Native 方法其实就是一个接口,但是它的具体实现是在外部由非 Java 语言写的。所以同一个 Native 方法,如果用不同的虚拟机去调用它,那么得到的结果和运行效率可能是不一样的,因为不同的虚拟机对于某个 Native 方法都有自己的实现,比如 Object 类的 hashCode 方法。这使得 Java 程序能够超越 Java 运行时的界限,有效地扩充了JVM。)
3)程序计数器:是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。(多线程的上下文切换就是依靠程序计数器恢复原来的运行状态的)

DEBUG查看线程的底层运行原理

在这里插入图片描述
可以看到上述代码我们的主线程(main方法)调用了方法1,而方法1又调用了方法2;
我们在调用方法1的地方打一个断点
在这里插入图片描述
每次运行main方法时会主动创建一个主线程
在这里插入图片描述
而且我们也知道其实真正存储线程数据的地方是在虚拟机栈里的栈帧,当我们运行main方法时就会为 main 方法生成一个栈帧,其中存储了局部变量表、操作数栈、动态链接、方法的返回地址等信息。
在这里插入图片描述
左边的 Frames 就是栈帧的意思,可以看见现在主线程中只有一个 main 栈帧;
右边的 Variables 就是该栈帧存储的局部变量表,可以看到现在 main 栈帧中只有一个局部变量,也就是方法参数 args。
我们看看DEBUG界面上的五个按钮是干啥的
1)Step Over:F8
程序向下执行一行,如果当前行有方法调用,这个方法将被执行完毕并返回,然后到下一行
2)Step Into:F7
程序向下执行一行,如果该行有自定义方法,则运行进入自定义方法(不会进入官方类库的方法)
3)Force Step Into:Alt + Shift + F7
程序向下执行一行,如果该行有自定义方法或者官方类库方法,则运行进入该方法(也就是可以进入任何方法)
4)Step Out:Shift + F8
如果在调试的时候你进入了一个方法,并觉得该方法没有问题,你就可以使用 Step Out 直接执行完该方法并跳出,返回到该方法被调用处的下一行语句。
5)Drop frame
点击该按钮后,你将返回到当前方法的调用处重新执行,并且所有上下文变量的值也回到那个时候。只要调用链中还有上级方法,可以跳到其中的任何一个方法。
我们点击step into运行方法1,可以看到又多了一个栈帧
在这里插入图片描述
再点击step into进入方法2,又多了一个方法2的栈帧
在这里插入图片描述
然后我们再step into的时候,会执行return n这个语句,将n的值返回给方法1里面的引用m,并且由于栈是先进后出,所以方法2的栈帧此时从虚拟机栈中销毁
在这里插入图片描述
然后点击 Step Over 执行完输出语句(Step Into 会进入 println 方法,Force Step Into 会进入 Object.toString 方法)
至此,method1 的使命全部完成,method1 栈帧会从虚拟机栈内存中被销毁。
最后再往下走一步,main 栈帧也会被销毁,这里就不再贴图了。

线程运行原理详解

上述的简单过程其实大家也对一个线程从创建到销毁有了一个大概的理解,接下来我们来了解一下线程运行时,Java运行时数据区域的各种变化
第一步——类加载
《深入理解 Java 虚拟机:JVM 高级实践与最佳实战 - 第 2 版》中是这样解释类加载的:虚拟机把描述类的数据从Class文件(字节码文件)加载到内存,并对字节码进行解析,校验和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
那么这些加载进来的字节码信息,就存放在方法区中。
在这里插入图片描述
所以上述代码例子的具体流程为(自己敲一遍看一下)

  1. 主线程调用main方法,于是该方法创建一个栈帧
  2. 那么main方法的args参数是哪里来的?是从堆里new出来的,main方法的返回地址就是程序的退出地址
  3. 再来看程序计数器,如果线程正在执行的是一个 Java 方法,程序计数器中记录的就是正在执行的虚拟机字节码指令的地址,也就是说此时 method1(10) 对应的字节码指令的地址会被放入程序计数器
  4. CPU根据程序计数器的指示,进入method1方法,然后方法1的栈帧也创建出来了
  5. 局部变量表和方法返回地址安顿好后,就可以开始具体的方法调用了,首先 10 会被传给 x,然后走到 int y = x + 1 这步,也就是程序计数器会被修改成这步代码对应的字节码指令的地址;
  6. 走到Object m=method2();的时候,又会创建一个方法2的栈帧
  7. 可以看到,method2 方法的第一行代码 Object n = new Object 会在堆中创建一个 Object 对象;
  8. 随后,走到 method2 方法中的最后一条 return n 语句,n 指向的堆中的地址就会被返回给 method1 中的 m,并且,满足栈后进先出的原则,method2 栈帧会从虚拟机栈内存中被销毁;
  9. 根据 method2 栈帧指向的方法返回地址,我们接着执行 method1 方法中的最后一条语句 System.out.println(m.toString()) ,执行完后,method1 栈帧也被销毁了;
  10. 随后返回到main方法,发现我们已经执行完了所有流程,此使main方法栈帧也销毁

多线程DEBUG底层运行原理

上述的是一个单线程的例子,那么多线程情况下又是什么情况呢?
其实原理和单线程是差不多的,因为虚拟机栈是每个线程私有的,互不干涉
比如我们在如下两个位置打断点DEBUG,然后运行
在这里插入图片描述
会发现创建了两个互不干涉的虚拟栈帧,当然涉及到多线程,那么不可避免的就是线程的上下文切换,简单的说,就是因为一些不可抗情况,导致当前线程不再运行,转而运行另外一个线程
具体原因

  1. 线程的CPU时间片用完
  2. 发生了垃圾回收
  3. 有更高优先级的线程进来了
  4. 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
    当线程的上下文切换发生时,也就是从一个线程 A 转而执行另一个线程 B 时,需要由操作系统保存当前线程 A 的状态(为了以后还能顺利回来接着执行),并恢复另一个线程 B 的状态。
    这个状态就**包括每个线程私有的程序计数器和虚拟机栈中每个栈帧的信息等,**显然,每次操作系统都需要存储这么多的信息,频繁的线程上下文切换势必会影响程序的性能。
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值