之前在网上看到的一些关于虚拟机和线程的讲解,讲得比较的通俗易懂,在这里做个整理,方便以后查看。其实理解下来,这些信息都能联系到之前在大学上的操作系统这门课,虚拟机、线程、cpu运行等等,这样理解和延伸就变容易很多了。(现在很后悔当时没好好听课啊。。)

虚拟机

Java程序运行在虚拟机上,而虚拟机又与操作系统打交道,最终通过二进制指令操纵电子电路运行,完成数据的读取、存储、运算和输出。

虚拟机在加载.class文件是,会在内存开辟一块区域“方法区”,专门用来存储类的基本信息,同时在“堆”区为这些类生成一个Class对象,作为类的镜像或模具,为反射提供基础。程序运行过程中,对象不断地生成和死亡,有的朝生暮死(大部分对象,例如方法内部生成的临时对象),有的壮年而亡,有的长命百岁,有的长生不死除非世界毁灭(虚拟机关闭)。对象生要吃喝死了要埋,所以虚拟机需要不停得去申请内存、回收内存。对象的生成方法有很多,new、反射等,对象回收的方法也很多,GC回收、标记-清除、复制、标记-整理等等。

垃圾回收,首先我们要知道什么是垃圾、为什么产生、如何回收。我所理解简单的垃圾回收,是后台启动一个线程,设置一定条件(间隔时间、资源阈值等),达到后扫描垃圾对象并清除,然后继续执行原来的程序。如果要更深层的研究,则需要考虑到效率、安全性等因素。举个例子:

对象生命周期不同,使用同一种回收机制(例如设置间隔时间)这样处理的效率是非常低的,对原程序的运行也会有不可预知的影响。那么我们可以这么改进:

  1. 根据对象的生命周期不同作为一个特性,用来划分不同的运行堆区,每个区采用不同的清理算法;
  2. 根据多核特性,启动多个回收线程一起跑;

由上述的内容,虚拟机优化就有两个大方向:各个区的大小如何最优划分、GC回收算法如何选择最优,映射到技术层面就是JVM参数调整。

多线程

程序运行的时候,占有cpu和内存资源,而cpu运算的时候需要从内存(很多时候是缓存)取值,然后放入寄存器参与运算,得到结果后先放入寄存器,再放回内存。程序执行的指令集也在放在寄存器中,它记录了当前程序执行的地址。一句话概括就是:程序=数据结构+算法。

线程执行是语言指令寄存器的,也就是当你切换线程的时候,需要从虚拟机的程序计数器(PC)把该线程的执行指令放到寄存器,当然线程涉及的其他资源也要切换,例如IO设备。这些都是需要耗费资源的,也就是常说的线程的上下文切换。cpu在执行程序的时候,需要准备程序执行指令所需的寄存器的值,以及所涉及的设备(文件系统、网络资源等),所以说线程创建是一个很大的开销,这也就解释了线程是cpu执行的最小单位了。

延伸一个问题,为什么单线程比多线程快?如上所述线程创建时cpu会分配资源,切换线程时其他资源也需要切换,在单线程模式下,资源都是线程本身的,不存在与其他线程共享与竞争其他资源的情况。

Java Thread的start方法和run方法的区别

由上述关于多线程的内容可知,start方法会在线程开始执行的时候准备线程所需的资源,而run方法则是相当于普通的方法调用,程序依然运行在主线程中。因此在多线程应用中,调用start方法后,线程必须的资源虚拟机会自动分配,我们所需要关心的只是告诉线程我们想做什么。

从此我们可以延伸出实现线程的方式:

  1. 继承Thread,重写run方法;
  2. 实现Runnable接口,实现run方法;
  3. 实现Callable接口,回调获取线程结果;

方法1使用了继承,方法2、3使用了组合,内部持有了所需实现的类,会让程序更加灵活。(这里可以对应到“多用组合少用继承”的开发原则)。

synchronized关键字与同步机制

一个数值,进入cpu运算,需要经过内存->多级缓存->寄存器。当多线程运算同一个数值是,需要把值从主内存拿到该线程工作的内存单元(寄存器)中,当一个线程计算完毕(CPU会优先把计算结果放到寄存器),还没来得及将数值刷新到主内存,这时候其他线程从主内存取到的是旧值。JVM运行的每个线程都有自己的线程栈,不同线程运行时,都需要复制主内存的一份副本到自身的工作内存。如何保证每个线程拿到的数据都是最新的,这就是同步机制。

synchronized关键字就是给数据上个锁,共享变量同一时刻只允许一个线程去操作,其他线程必须等待锁释放后才可以进入。这里可以联系到上厕所模型,然后就可以延伸出锁的类型了。

但有些时候,我们不关心共享值在被谁操作,我们只关心这个值当前是什么。所以就有了volatile。很多博客对该关键词的描述是:保证可见性,不保证原子性。我们来拆解(拆分理解)一下这句话。

一个共享变量声明为volatile,等于告诉虚拟机控制所有的线程:这个变量有点帅,要请他需要他的老家(主内存)来,回来时也要尽快送回去。所以,CPU计算时从主内存取值,计算完毕后直接存入主内存,不会写到缓存了。这就是所谓的“可见性”,这个值当前是什么,我们是完全知道的。至于原子性,由上可知谁都可以取值来进行运算。