前言

前两天和朋友探讨技术的时候有聊到JVM和JDK这一块,聊到这里两个人就像高山流水遇知音那是根本停不下来,事后我想着趁现在印象还比较深刻就把这些东西整理起来分享给大家来帮助更多的人吧。话不多说,满满的干货都整理在下面了!

JVM探究

jvm的位置

jvm的体系结构

堆里面有垃圾,需要被GC回收

栈里面是没有垃圾的,用完就弹出去了,栈里面有垃圾,程序就崩了,执行不完main方法。

Java栈,本地方法栈,程序计数器里面是不可能存在垃圾的。也就不会有垃圾回收。

所谓的jvm调优就是在堆里面调优了,jvm调优99%都是在方法区和堆里面进行调优的。

类加载器

public class Car {
    public static void main(String[] args) {
        Car car1 = new Car();
        Car car2 = new Car();
        Car car3 = new Car();
        System.out.println(car1.hashCode());
        System.out.println(car2.hashCode());
        System.out.println(car3.hashCode());
        Class<? extends Car> aClass1 = car1.getClass();
        Class<? extends Car> aClass2 = car2.getClass();
        Class<? extends Car> aClass3 = car3.getClass();
        System.out.println(aClass1.hashCode());
        System.out.println(aClass2.hashCode());
        System.out.println(aClass3.hashCode());

    }
}

作用:加载class文件 - 类似new Student();

类是一个模板,是抽象的,而new出来的对象是具体的,是对这个抽象的类的实例化

1.虚拟机自带的加载器

2.启动类(根)加载器

3.扩展加载器

4.应用程序(系统类)加载器

ClassLoader classLoader = aClass1.getClassLoader();
System.out.println(classLoader);//AppClassLoader 应用程序加载器

System.out.println(classLoader.getParent());//ExtClassLoader 扩展类加载器

System.out.println(classLoader.getParent().getParent());//null 1.不存在  2.Java程序获取不到

1.类加载器收到类加载的请求

2.将这个请求向上委托给父类加载器去完成,一直向上委托,直到根加载器

3.启动类加载器会检查是否能够加载当前这个类,能加载就结束,使用当前加载器,否则,抛出异常,通知子类加载器进行加载。

4.重复步骤3

若都找不到就会报 Class Not Found

null:Java调用不到,可能编程语言是C写的,所以调不到

Java =C+±- 去掉C里面比较繁琐的东西 指针,内存管理(JVM帮你做了)

双亲委派机制

双亲委派机制:安全

APP–>EXC–BOOTStrap(根目录,最终执行)

当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。

在src下创建Java.lang包,创建一个String类

package java.lang;

public class String {
    public String toString(){
        return "hello";
    }

    public static void main(String[] args) {
        String s = new String();
        System.out.println(s.getClass().getClassLoader());
        s.toString();
    }
}

执行结果

它会去最终的BOOTStrap里面的String类里面去执行,找到执行类的位置,发现里面没有要执行的mian方法,所以会报这个错。

在src下创建类Student

public class Student {
    public String toString(){
        return "HELLO";
    }

    public static void main(String[] args) {
        Student student = new Student();
        System.out.println(student.getClass().getClassLoader());
        System.out.println(student.toString());
    }
    
}

执行结果

如上图可见最终是在APP里面执行的,成功输出HELLO语句

双亲委派机制的作用

1、防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
2、保证核心.class不能被篡改。通过委托方式,不会去篡改核心.class,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。

沙箱安全机制

​ Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?沙箱是一个限制程序运行的环境,沙箱机制就是将Java代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏,沙箱主要限制系统资源访问,那系统资源包括什么?CPU,内存,文件系统,网格,不同级别的沙箱对这些资源访问的限制也是可以不一样。

​ 所以的Java程序运行都可以指定沙箱,可以定制安全策略。

​ 在Java中将执行程序分为本地代码呵远程代码两种,本地代码默认视为可信任的,而远程代码则被看作是不受信任的,对于授权的本地代码,可以访问一切本地资源,而对于非授信的远程代码在早期的Java实现中,安全依赖于沙箱机制,如下图jdk1.0安全模型

但如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现,因此在后续的Java1.1版本中,针对安全机制做了改进,增加了安全策略,允许用户指定代码本地资源的访问权限,如下图所示JDK1.1安全模型

​ 在Java1.2版本中,再次改进了安全机制,增加了代码签名,不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制,如下图所JDK1.2安全模型

当前最新的安全机制实现,则引入域(Domain)的概念,虚拟机会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来各种需求的资源进行访问,虚拟机中不同的受保护域,对应不一样的权限,存在不同域中的类文件就具有了当前域的全部权限,如下图所示,最新的安全模型

组成沙箱的基本组件:

字节码校验器(bytecode verifier):确保Java类文件遵循Java语言规范,这样可以帮助Java程序实现内存保护,但并不是所有的类文件都会经过字节码校验,比如核心类。

类加载器(class loader):其中类加载器在3个方面对Java沙箱起作用

​ 它防止恶意代码去干涉善意的代码;//双亲委派机制

​ 它守护了被信任的类库边界;

​ 它将代码归入保护域,确定了代码可以进行哪些操作;

​ 虚拟机为不同的类加载器载入的类提供不同的命名空间,命名空间由一系列唯一的名称组成,每一个被装载的类将有一个名字,这个命名空间由Java虚拟机为每一个类加载器维护的,它们互相甚至不可见。

​ 类加载器采用的机制是双亲委派模式。

​ 虚拟机为不同的类加载开始加载,外层恶意同名类得不到加载从而无法使用;

​ 由于严格通过包来区分了访问域:外层恶意的类通过内置代码也无法获得权限访问到内置类,破坏代码就自然无法生效。

存取控制器(access controller):存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定。

​ 安全管理器 (security manager):是核心API和操作系统之间的主要接口,实现权限控制,比存取控制器优先级高。

​ 安全软件包(security package):java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括:

​ 安全提供者

​ 信息摘要

​ 数字签名 kettools https(需要证书)

​ 加密

​ 鉴别

Native

凡是带了native关键字,说明Java的的作用范围达不到了,会去调用底层C语言的库!

会进入本地方法栈

调用本地方法本地接口 JNI

JNI的作用:扩展Java的使用,融合不同的编程语言为Java所用,最初:C,C++

Java诞生的时候C ,C++比较火,Java想要立足,必须要有调用C,C++的程序。

他在内存区域专门开辟了一块标记区域 :Native Method Stack,登记native方法

在最终执行的时候,加载本地方法库中的方法通过JNI

Java程序驱动打印机,管理系统,掌握即可,在企业级应用中较为少见。

​ 目前该方法使用使用的越来越少了,除非 是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见,因为现在的异构领域间通信很发达,比如可以使用Socjet通信,也可以使Web Service等等 ,不多做介绍!

PC寄存器

​ 程序计数器:Program Counter Register

​ 每个线程都有一个程序计数器,要线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向像一条指令的地址,也即将要执行的指令代码),在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。

方法区

Method Area 方法区

​ 方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,在此区域属于共享区间

​ 静态变量,常量,类信息(构造方法,接口定义),运行时的常量池存在方法区中,但是实例变量存在堆内存中,与方法区无关。

static,final,Class,常量池

栈是一种数据结构

​ 程序 = 数据结构 + 算法 :持续学习~

​ 程序 = 框架 + 业务逻辑 :吃饭~

栈:先进后出,后进先出

队列:先进先出(FIFO:First input First Output)

方法运行完成以后,就会被栈弹出去

两个方法互相调,就会导致栈溢出

public class Inn {
    public static void main(String[] args) {
        new Inn().test();
    }
    public void test(){
        a();
    }
    public void a(){
        test();
    }
    //a调test,test调a
}

运行结果

栈:栈内存,主管程序的运行,生命周期和线程同步,也就是线程如果都结束了,栈也就变成空的了;

线程结束,占内存也就释放了,对于栈来说,不存在垃圾回收问题,一旦线程结束,栈就没了。

栈:8大基本类型+对象引用+实例的方法

栈运行原理:栈帧

函数调用过程中,肯定需要空间的开辟,而调用这个函数时为该函数开辟的空间就叫做该函数的栈帧

程序正在执行的方法一定在栈的顶部,执行完就会弹出去

栈1在运行完成之后就会弹出去,然后栈2在去执行,栈2执行完,程序也就结束了

栈满了:StackOverflowError

栈+堆+方法区 :交互

如下图:对象在内存中实例化的过程

三种jvm

Sun 公司 HotSpot Java HotSpot™ 64-Bit Server VM (build 25.77-b03, mixed mode)
BEA :JRockit
IBM :J9 VM
我们用的是hotspot


Heap(堆):一个jvm只有一个,堆内存的大小是可以调节的。

类加载器读取了类文件后,一般会把什么东西放到堆中呢?类,方法,常量,保存我们所有引用类型的真实对象。

堆内存中还要细分为三个区域:

新生区(伊甸园区)

养老区

永久区

GC垃圾回收,主要是在伊甸园区和养老区,

假设内存满了,会报OOM,堆内存不够,堆溢出

在jdk8以后,永久存储区改了个名字叫(元空间)

新生区

它是一个类:诞生和成长的地方,甚至死亡;

新生区分为 伊甸园区和幸存者区

伊甸园区;所有的对象都是在伊甸园区里new出来的

幸存区:(0,1)

当伊甸园区满了以后,会触发轻GC,对伊甸园区进行垃圾回收,当某个对象通过GC幸存下来以后,就会进入到幸存者区,依次不断的循环,当幸存0区和1区也满了的时候,在经历过多次GC以后,活下来的对象,也就是被重新引用的对象就会进入到老年区。而当老年区也满了的时候,就会触发重GC,重CG除了会去清理老年区的,还会伊甸园区和幸存0区1区的所有垃圾全清理掉。而在重GC清除下,活下来的就会进入到养老区。当重GC清理完毕以后,新生区和养老区还是都满了,这个时候就会报堆溢出的报错。

真理:经过研究,99%对象都是临时对象。

老年区

新生区里面GC不掉的对象就会去到老年区

永久区

这个区域常驻内存的,用来存放JDK自身携带的Class对象,Interface元数据,存储的是Java运行时的一些环境或类信息,这个区域不存在垃圾回收!关闭VM虚拟机就会释放这个区域的内存。

一个启动类加载了大量的第三方jar包,Tomcat部署了太多的应用,大量动态生成的反射类,不断的被加载,直到内存满,就会出现OOM;

jdk1.6之前:永久代,常量池是在方法区之中

jdk1.7:永久代,但是慢慢的退化,去永久代,常量池在堆中

jdk1.8之后:无永久代,常量池在元空间。

逻辑上存在,物理上不存在

堆内存调优

堆内存满了,该如何处理?

public static void main(String[] args) {
    long max = Runtime.getRuntime().maxMemory();
    long total = Runtime.getRuntime().totalMemory();
    System.out.println("max="+max+"字节\t"+(max/(double)1024/1024)+"MB");
    System.out.println("total="+max+"字节\t"+(total/(double)1024/1024)+"MB");
}

​ 先尝试把堆内存空间扩大,假如还是用原来的代码跑,继续包堆溢出的错,我们就该去考虑考虑自己代码那块有问题,可能是有垃圾代码或者是死循环代码,它在不断的占用内存空间

​ 分析内存,看一下那个地方出现了问题(专业工具)

​ 调优代码-Xms1024m -Xmx1024m -XX:+PrintGCDetails

Xms后面填计算机给jvm分配的内存,Xmx后面填Jvm初始的内存值

在一个项目中,突然出现了OOM故障,那该如何排除研究为什么出错~

能够看到代码第几行出错;
Dubug:一行行分析代码!
MAT,Jprofiler作用:

分析Dump内存文件,快速定位内存泄漏;
获得堆中的数据
获得大的对象~
。。。。

GC

jvm在进行垃圾回收时,并不是对这三个区域统一回收,大部分时候,回收但是新生代

新生代

幸存区(from,to)

老年区

GC两种类:轻GC(普通的GC),重GC(全局GC)

GC常用算法

标记清除法

​ 最基础的收集算法是“标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:

​ 首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象,之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。

​ 它的主要缺点有两个:一个是效率问题,标记和清除过程的效率都不高;另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。标记-清除算法的执行过程(需要较大内存时却不够了就要回收一次)

复制算法
为了解决效率问题,一种称为“复制”(Copying)的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,未免太高了一点。

标记-整理算法

复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

分代收集算法(并不是一种新的思想,只是将java堆分成新生代和老年代,根据各自特点采用不同算法)
当前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法,这种算法并没有什么新的思想,只是根据对象的存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。
新生代–复制算法。老年代–标记-整理算法。

字节码引擎

1.概述
​ Java虚拟机字节码执行引擎是jvm最核心的组成部分之一,所有的 Java 虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果,下面将主要从概念模型的角度来讲解虚拟机的方法调用和字节码执行。

2.执行引擎的解释和作用

​ 类加载器将字节码载入内存之后,执行引擎以Java 字节码指令为单元,读取Java字节码。问题是,现在的java字节码机器是读不懂的,因此还必须想办法将字节码转化成平台相关的机器码(也就是系统能识别的0和1)。这个过程可以由解释器来执行,也可以有即时编译器(JIT Compiler)来完成

​ 具体步骤如下图

执行引擎内部包括如下

语法糖

1.概述
​ 语法糖是一种用来方便程序员代码开发的手段,简化程序开发,但是不会提供实质性的功能改造,但可以提高开发效率或者语法的严谨性或者减少编码出错的机会。
​ 总而言之,语法糖可以看作是编译器实现的一种小把戏。

2.泛型和类型擦除

​ 泛型的本质是参数化类型,也就是操作的数据类型本身也是一个参数。这种参数类型可以用在类,接口,方法中,分别叫泛型类,泛型接口,泛型方法。

​ 但是java的泛型是一个语法糖,并非是真实泛型,只在源码中存在,List和List 在编译之后,就是List 并在相应的地方加上了类型转换代码。这种实现方式叫类型擦除,也叫伪泛型。

但是,擦除法所谓的擦除,仅仅是对方法的code属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息,这也是我们能通过反射手段获取参数化类型的根本依据。

泛型:

public class b {
    public static void main(String[] args) {
        Map<String,String> map = new HashMap<>();
        map.put("hello","a");
        System.out.println(map.get("hello"));
    }
}

实际上:

public class b {
    public b(){
        
    }
    public static void main(String[] args) {
        Map<String,String> map = new HashMap<>();
        map.put("hello","a");
        System.out.println(map.get("hello"));
    }
}

3…自动装箱和遍历循环

自动装箱和遍历循环

public class b {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1,2,3,4);
        for (Integer integer:
             list) {
            System.out.println(integer);
        }
    }
}

实际上

public class b {
    public static void main(String[] args) {
        List<Integer> list =                    Arrays.asList(Integer.valueOf(1),Integer.valueOf(2),Integer.valueOf(3),Integer.valueOf(4));
        Iterator<Integer> iterator = list.iterator();
        while (iterator.hasNext()){
            Integer next = (Integer) iterator.next();
            System.out.println(next);
        }

    }

自动装箱用了Integer.valueOf
for循环用了迭代器

4.条件编译

​ —般情况下,程序中的每一行代码都要参加编译。但有时候出于对程序代码优化的考虑,希望只对其中一部分内容进行编译,此时就需要在程序中加上条件,让编译器只对满足条件的代码进行编译,将不满足条件的代码舍弃,这就是条件编译。

反编译之前

public static void main(String[] args) {
    if(true){
        System.out.println("hello");
    }else{
        System.out.println("beybey");
    }
}

反编译之后

public static void main(String[] args) {
        System.out.println("hello");
}

首先,我们发现,在反编译后的代码中没有System.out.println(“beybey”);,这其实就是条件编译。

当if(tue)为true的时候,编译器就没有对else的代码进行编译。

所以,Java语法的条件编译,是通过判断条件为常量的if语句实现的。根据if判断条件的真假,编译器直接把分支为false的代码块消除。通过该方式实现的条件编译,必须在方法体内实现,而无法在正整个Java类的结构或者类的属性上进行条件编译。

运行期优化

​ Java 语言的 “编译期” 其实是一段 “不确定” 的操作过程,因为它可能是指一个前端编译器把 .java 文件转变成 .class 文件的过程;也可能是指虚拟机的后端运行期编译器(JIT 编译器,Just In Time Compiler)把字节码转变成机器码的过程;还可能是指使用静态提前编译器(AOT 编译器,Ahead Of Time Compiler)直接把 *.java 文件编译成本地机器代码的过程

1.解释器与编译器

什么是解释器?

​ 大概意思:

​ 在计算机科学中,解释器是一种计算机程序,它直接执行由编程语言或脚本语言编写的代码,并不会把源代码预编译成机器码。一个解释器,通常会用以下的姿势来执行程序代码:

​ 分析源代码,并且直接执行。
​ 把源代码翻译成相对更加高效率的中间码,然后立即执行它。
​ 执行由解释器内部的编译器预编译后保存的代码
​ 可以把解释器看成一个黑盒子,我们输入源码,它就会实时返回结果。
​ 不同类型的解释器,黑盒子里面的构造不一样,有些还会集成编译器,缓存编译结果,用来提高执行效率(例如 Chrome V8 也是这么做的)。
​ 解释器通常是工作在「运行时」,并且对于我们输入的源码,是一行一行的解释然后执行,然后返回结果。

​ 什么是编译器?

​ 源文件经过编译器编译后才可生成二进制文件,编译过程包括预处理、编译、汇编和链接,日常交流中常用“编译”称呼此四个过程

2.编译对象与触发条件

"热点代码"分两类,

​ 第一类是被多次调用的方法-这是由方法调用触发的编译。

​ 第二类是被多次执行的循环体 – 尽管编译动作是由循环体所触发的,但编译器依然会以整个方法(而不是单独的循环体)作为编译对象。

判断一段代码是不是热点代码,是不是需要触发即时编译,这样的行为称为热点探测(Hot Spot Detection);热点探测判定方式有两种:

​ 第一种是基于采样的热点探测

​ 第二种是基于计数器的热点探测

HotSpot虚拟机中使用的是基于计数器的热点探测方法,因此它为每个方法准备了两类计数器:方法调用计数器(Invocation Counter)和回边计数器(Back EdgeCounter)。确定虚拟机运行参数的前提下,这两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就会触发JIT编译。

3.编译优化技术

经典优化技术

语言无关的经典优化技术之一:公共子表达式消除。
语言相关的经典优化技术之一:数组范围检查消除。
最重要的优化技术之一:方法内联。
最前沿的优化技术之一:逃逸分析。
公共子表达式消除

    int d= (c * b)*12+a+ (a + b * c)
 
//编译器检测到“c * b”与“b* c”是一样的表达式,而且在计算期间b与c的值是不变的。
        int d=E*12+a+(a+E);

数组边界检查消除

if (foo != null) {
        return foo.value;
        } else {
        throw new NullPointerException();
        }

        # 虚拟机隐式优化;
        try {
        return foo.value;
        } catch (Segment_Fault e) {
        uncommon_trap(e);

4 Java与C/C++的编译器对比

第一,因为即时编译器运行占用的是用户程序的运行时间,具有很大的时间压力,它能提供的优化手段也严重受制于编译成本

第二,Java语言是动态的类型安全语言,这就意味着需要由虚拟机来确保程序不会违反语言语义或访问非结构化内存

第三,Java语言中虽然没有virtual关键字,但是使用虚方法的频率却远远大于C/C++语言

Java内存模型与线程

1. 硬件效率与一致性

除了增加高速缓存之外,为了使得处理器内部的运算单元尽可能被充分利用,处理器可能会对输入代码进行乱序执行优化,处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序与代码中的顺序一致。

2. Java内存模型

​ 主内存与工作内存

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

​ 为了获取较好的执行效能,Java内存模型并没有限制执行引擎使用处理器的特定寄存器或缓存来和主内存进行交互,也没有限制即时编译器进行调整代码执行顺序这类优化操作。

​ 内存间的交互操作

​ lock(锁定):作用于主内存的变量,它把一个变量标志为一条线程独占的状态。
​ unlock(解锁):作用于主内存中的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁 定。
​ read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
​ load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
​ use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎。
​ assign(赋值):作用于工作内存的变量,它把一个从执行引擎接受到的值赋给工作内存的变量。
​ store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
​ write(写入):作用于主内存中的变量,它把store操作从主内存中得到的变量值放入主内存的变量中。

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

​ 原子性(Atomicity):由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、和write。

​ 可见性(Visibility):可见性是指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。J
​ 有序性(Ordering):如果在本线程内观察,所有的操作都是有序的;

3.Java与线程

​ 线程的实现

​ 使用内核线程实现

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

使用用户线程实现

​ 从广义上来讲,一个线程只要不是内核线程,就可以认为是用户线程,因此,从这个定义上来讲,轻量级进程也属于用户线程,但轻量级进程的实现始终是建立在内核之上的,许多操作都进行系统调用,效率会受到限制。
而狭义上的用户线程指的是完全建立在用户空间的线程库上,系统内核不能感知线程存在的实现。
​ 使用用户线程的优势在于不需要系统内核的支援。劣势也在于没有系统内核的支援,所有的线程操作都需要用户程序自己处理。

使用用户线程加轻量级进程混合实现

​ 在这种混合模式下,即存在用户线程,也存在轻量级进程。用户线程还是完全建立在用户空间中,因此用户线程的创建、切换、析构等操作依然廉价,并且可以支持大规模的用户线程并发

4.Java线程的状态转化

新建(New):创建后尚未启动的线程处于这种状态。
运行(Runable):Runable包括了操作系统线程状态中的Running和Ready,也就是说处于此种状态的线程可能正在执行,也可能正在等待CPU为它分配执行时间。
无限期等待(Waiting):处于这种状态下的线程不会被分配CPU执行时间,他们要等待被其他线程显示唤醒。
限期等待(Timed Waiting):处于这种状态下的线程也不会被分配CPU执行时间,不过无须等待被其他线程显示唤醒,在一定时间之后它们由系统自动唤醒。
阻塞(Blocked):线程被阻塞了,“阻塞状态”与“等待状态”的区别是:“阻塞状态”在等待着获取一个排他锁,这个事件将在另外一个线程放弃这个锁的时候发生。
结束(Terminate):已经终止的线程的线程状态,线程已经结束执行。

线程安全与锁优化

1.线程安全

​ 当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的

2.Java 语言中的线程安全

​ 2.1不可变

​ 在 Java 语言中,不可变线程一定是安全的,无论是对象的方法实现还是方法的调用者,都不需要采取任何的线程安全保障措施

​ 其中最简单的就是把对象中带有状态的变量都声明为 final,这样在构造函数结束之后,它就是不可变的

​ 2.2绝对线程安全

private static Vector<Integer> vector = new Vector<Integer>();  
1
public static void main(String[] args) {
while (true) {
for (int i = 0; i < 10; i++) {
vector.add(i);
}

    Thread removeThread = new Thread(new Runnable() {  
        @Override  
        public void run() {  
            for (int i = 0; i < vector.size(); i++) {  
                vector.remove(i);  
            }  
        }  
    });  
      
    Thread printThread = new Thread(new Runnable() {  
        @Override  
        public void run() {  
            for (int i = 0; i < vector.size(); i++) {  
                System.out.println(vector.get(i));  
            }  
        }  
    });  
      
    removeThread.start();  
    printThread.start();  
      
    // 不要同时产生过多的线程,否则会导致操作系统假死  
    while (Thread.activeCount() > 20);  
}  
}

3.相对线程安全

相对的线程安全就是我们通常意义上所讲的线程安全,它需要保证对这个对象单独的操作是线程安全,我们在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。

4.线程兼容

线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用,我们平常说一个类不是线程安全的,绝大多数时候指的是这一种情况。

5.线程对立

线程对立是指无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码。由于 Java 语言天生就具备多线程特性,线程对立这种排斥多线程的代码是很少出现的,而且通常都是有害的,应当尽量避免。

线程安全的实现方法

1.互斥同步

同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个(或者是一些,使用信号量的时候)线程使用,而互斥是实现同步的一种手段,互斥是方法,同步是目的

2.非阻塞同步

测试并设置,获取并增加,交换,比较并交换,加载连接 / 条件存储

3.无同步方案

​ 要保证线程安全,不一定非要保证线程同步,还可以有其他的方案

​ 1.可重入代码
​ 2.线程本地存储

锁优化

自旋锁和自适应自旋锁

如果锁在很短的时间内释放了,那么自旋的效果就很好

偏向锁

​ 偏向锁的意思是这个锁会偏向第一个获取到他的锁,如果在接下来执行的过程中,该锁一直没有被其他的锁获取的话,则持有偏向锁的线程永远不需要再进行同步.一旦有新的线程试图获取该锁,偏向模式会被撤销.撤销后进入无锁状态.这里会改变对象头的关于偏性模式的标志位和关于锁的标志位

轻量级锁
当使用轻量级锁(锁标识位为00)时,线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中

锁粗化

​ 这个原则大部分时间是对的但是如果一个系列的连续操作都是对同一个对象反复的加锁和解锁,甚至加锁操作出现在循环体之中,即使没有线程竞争,频繁的进行互斥同步的操作也会导致不必要的性能损耗.

锁消除

public String concatString(String s1, String s2){
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb.toString();
}

​ 我们发现sb的引用被限制在了concatStirng方法里面他永远不可能被其他线程访问到,因此虽然这里有锁但是可以被安全的消除掉.在解释执行时这里仍然会枷锁,但是经过服务端编译器即时编译后,这段代码会自动忽略所有的同步措施直接执行.

最后

大家看完有什么不懂的可以在下方留言讨论,也可以关注我私信问我,我看到后都会回答的。也欢迎大家关注我的公众号:前程有光,金三银四跳槽面试季,整理了1000多道将近500多页pdf文档的Java面试题资料,文章都会在里面更新,整理的资料也会放在里面。谢谢你的观看,觉得文章对你有帮助的话记得关注我点个赞支持一下!


前程有光
936 声望618 粉丝