记录一部分基础的笔记
1、缓存池
new Integer(123) 每次都会新建一个对象;
Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。
2、String
String声明为final 不可被继承,Integer包装类等也不能被继承
String源码:Java8内部使用char数组存储数据,Java9之后使用byte数组存储,coder表示编码类型
String源码中value数组被声明为final,value数组在初始化之后不能引用其他数组。
String内部没有改变value数组的方法,可保证String不可变。
3、final
final关键字:
1、使用final定义的类不能有子类--不能被继承
2、使用final定义的方法不能被子类覆写
--private 方法隐式地被指定为 final,如果在子类中定义的方法和父类中的一个 private 方法签名相同--不是重写是新定义
3、对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;
如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。被引用的对象本身可以修改
4、String不可变的好处
1、可以缓存哈希值--用作HashMap的key,不可变是hash值也不可变,只需进行一次计算。--加快字符串处理速度
2、便于实现字符串池(String pool)--在堆中开辟一块存储空间String Pool,如果String对象已被创建,从池中取得引用
3、保证安全性---用作参数,参数不可变。网络连接中
4、多线程安全---不可变对象不能被写,保证线程安全。
5、StringBuffer 和 StringBuilder
StringBuffer 和 StringBuilder 可变
StringBuilder 不是线程安全的,速度快
StringBuffer 是线程安全的,内部使用 synchronized 进行同步
6、 intern()
String 的 intern() 方法在运行过程将字符串添加到 String Pool 中。
String s1 = new String("aaa");
String s2 = new String("aaa");
System.out.println(s1 == s2); // false
String s3 = s1.intern();
String s4 = s2.intern();
System.out.println(s3 == s4); // true
String s5 = "bbb";
String s6 = "bbb";
System.out.println(s5 == s6); // true
7、new String(“abc”)
使用这种方式一共会创建两个字符串对象(前提是 String Pool 中还没有 "abc" 字符串对象):
1、"abc" 属于字符串字面量,因此编译时期会在 String Pool 中创建一个字符串对象,指向这个 "abc" 字符串字面量;
2、而使用 new 的方式会在堆中创建一个字符串对象。
javap -verbose 进行反编译
// ...
Constant pool:
// ...
#2 = Class #18 // java/lang/String
#3 = String #19 // abc
// ...
#18 = Utf8 java/lang/String
#19 = Utf8 abc
// ...
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: new #2 // class java/lang/String
3: dup
4: ldc #3 // String abc
6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
9: astore_1
// ...
在 Constant Pool 中,#19 存储这字符串字面量 "abc",#3 是 String Pool 的字符串对象,它指向 #19 这个字符串字面量。
在 main 方法中,0: 行使用 new #2 在堆中创建一个字符串对象,并且使用 ldc #3 将 String Pool 中的字符串对象作为String 构造函数的参数。
new一个对象的过程
1,首先到常量池中找类的带路径全名,然后检查对应的字节码是否已被加载,解析,验证,初始化,如果没有先执行类加载过程(class.forname())。
2,类加载过程完成后,虚拟机会为对象分配内存。分配内存有两种方式,根据使用的垃圾收集器的不同使用不同的分配机制。
(1)指针碰撞,当虚拟机使用复制算法或标记整理算法实现的垃圾收集器时,内存区域都是规整的,这时候使用指针碰撞分配内存,用过的内存放在一边,空闲的内存在另一边,中间用一个指针作为分界点,当需要为新对象分配内存时只需把指针向空闲的一边移动一段与对象大小相等的距离。
(2)空闲列表,当虚拟机使用标记清除算法实现的垃圾收集器时,内存都是碎片化的,那虚拟机就要记录哪块内存是可用的,当需要分配内存时,找一块足够大的内存空间给对象实例,并更新记录。
3,设置对象头信息,如所属类,元数据信息,哈希码,gc分代年龄,等等。
4,调用对象的init()方法,根据传入的属性值给对象属性赋值。
5,在线程栈中新建对象引用,并指向堆中刚刚新建的对象实例。
8、引用传递和值传递
Java 的参数是以值传递的形式传入方法中,而不是引用传递。
值传递是传递实参副本,函数修改不会影响实参;引用传递是传递实参地址,函数修改会影响实参。
然而在 Java 中,没有引用的概念,Java 中只要定义变量就会开辟一个存储单元。因此,对 Java 语言来说只有值传递,没有引用传递是正确的。
值传递:指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
引用传递:是指在调用函数时将实际参数的地址直接传递到函数中(的形参),那么在函数中对参数所进行的修改,将影响到实际参数。
引用传递:形参为指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进行的操作。
9、字面量
Java直接量/字面量—是指在程序中通过源代码直接给出的值
能指定直接量的通常只有三种类型:基本类型、字符串类型和 null 类型。具体如下 8 种类型:
1)int 类型的直接量---整型数值,可分为二进制( 0B 或 0b)、十进制、八进制(以 0 开头)和十六进制( 0x 或 0X ) 4 种,
2)long 类型的直接量---在整型数值后添加 l 或 L 后就变成了 long 类型的直接量。例如 3L、0x12L(对应十进制的 18L)。
3)float 类型的直接量---在一个浮点数后添加 f 或 F 就变成了 float 类型的直接量,这个浮点数可以是标准小数形式,也可以是科学计数法形式。
4)double 类型的直接量---直接给出一个标准小数形式或者科学计数法形式的浮点数就是 double 类型的直接量。例如 5.34、3.14E5。
5)boolean 类型的直接量---只有 true 和 false。
6)char 类型的直接量---有三种形式,分别是用单引号括起来的字符、转义字符和 Unicode 值表示的字符。例如‘a’,‘\n’和‘\u0061’。
7)String 类型的直接量----一个用双引号括起来的字符序列就是 String 类型的直接量。
8)null 类型的直接量 --- 即 null。--可以赋给任何引用类型的变量,用以表示这个引用类型变量中保存的地址为空,即还未指向任何有效对象。
10、Java 不能隐式执行向下转型,因为这会使得精度降低。
11、从 Java 7 开始,可以在 switch 条件判断语句中使用 String 对象, 不支持 long、float、double
12、static关键字
1、静态变量
1、静态变量: 类变量,属于这个类,类所有实例共享静态变量,可直接通过类名访问。在内存中只存在一份
2、实例变量:每创建一个实例就会产生一个实例变量,与该实例共存亡。
2、静态方法
静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须有实现,也就是说它不能是抽象方法。
只能访问所属类的静态字段和静态方法,方法中不能有 this 和 super 关键字,因此这两个关键字与具体对象关联。
3、静态语句块
在类初始化时运行一次。
4、静态内部类
非静态内部类依赖于外部类的实例,也就是说需要先创建外部类实例,才能用这个实例去创建非静态内部类。
而静态内部类不需要,可以直接创建静态内部类。
静态内部类不能访问外部类的非静态的变量和方法。
5、静态导包
在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低。
13、初始化顺序
1、按代码中的顺序初始化静态变量和静态语句块
2、实例变量和普通语句块
3、构造函数的初始化
存在继承的情况下,初始化顺序:
1、父类(静态变量、静态语句块)
2、子类(静态变量、静态语句块)
3、父类(实例变量、普通语句块)
4、父类(构造函数)
5、子类(实例变量、普通语句块)
6、子类(构造函数)
14、equals和==
等价关系:满足自反性、对称性、传递性、一致性、与null比较false
对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。
对于引用类型,== 判断两个变量是否引用同一个对象,而 equals() 判断引用的对象是否等价。
equals源码实现:
检查是否为同一个对象的引用,如果是直接返回 true;
检查是否是同一个类型,如果不是,直接返回 false;----自己查看源码是使用instanceof判断是否同一类型
将 Object 对象进行转型;
判断每个关键域是否相等。---自己查看源码是对value数组进行逐一对比
1、String 中的 equals ⽅法是被重写过的,因为 object 的 equals ⽅法是比较对象的内存地址,⽽ String 的 equals ⽅法⽐较的是对象的值。
2、当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引⽤。如果没有就在常量池中重新创建⼀个 String 对象
15、equals()和 hashcode()
两个对象相等,hashcode一定相等
两个对象不等,hashcode不一定不等
hashcode相等,两个对象不一定相等
hashcode不等,两个对象一定不等
等价的两个对象哈希值一定相同,但是哈希值相同的两个对象不一定等价。
这是因为计算哈希值的方法具有随机性,两个值不同的对象可能计算出相同的哈希值。
在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证等价的两个对象哈希值也相等。
理想的哈希函数应当具有均匀性,即不相等的对象应当均匀分布到所有可能的哈希值上
哈希函数将所有域的值考虑进来,每个域当成R进制某一位,组成一个R进制的整数。
R 一般取 31,因为它是一个奇素数,
如果是偶数的话,当出现乘法溢出,信息就会丢失,因为与 2 相乘相当于向左移一位,最左边的位丢失。
并且一个数与 31 相乘可以转换成移位和减法:31*x == (x<<5)-x,编译器会自动进行这个优化。
@Override
public int hashCode() {
int result = 17;
result = 31 * result + x;
result = 31 * result + y;
result = 31 * result + z;
return result;
}
16、toString()
toString()默认返回 ToStringExample@4554617c 这种形式,
其中 @ 后面的数值为散列码的无符号十六进制表示。
17、clone()
clone() 是 Object 的 protected 方法,它不是 public,一个类不显式去重写 clone(),其它类就不能直接去调用该类实例的 clone() 方法。
clone() 方法并不是 Cloneable 接口的方法。但是要调用clone() 方法必须要实现Clonable接口。
public class CloneExample implements Cloneable {…}
拷贝:
浅拷贝:拷贝对象和原始对象的引用类型引用同一个对象
深拷贝:拷贝对象和原始对象的引用类型引用不同对象-----新建一个新的对象
----最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。
//浅拷贝
@Override
protected ShallowCloneExample clone() throws CloneNotSupportedException {
return (ShallowCloneExample) super.clone();
}
//深拷贝
@Override
protected DeepCloneExample clone() throws CloneNotSupportedException {
DeepCloneExample result = (DeepCloneExample) super.clone();
result.arr = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
result.arr[i] = arr[i];
}
return result;
}
18、访问权限
如果子类的方法重写了父类的方法,那么子类中该方法的访问级别不允许低于父类的访问级别。
字段决不能是公有的,可以使用公有的 getter 和 setter 方法来替换公有字段,这样的话就可以控制对字段的修改行为。
19、抽象类和接口
抽象类不能被实例化,只能被继承。
接口默认是public abstract的,java8之前可以看成一个完全抽象的类,内部全是抽象方法。
接口的字段默认都是 static 和 final 的。
从 Java 8 开始,接口也可以拥有默认的方法实现---default,也可以定义静态方法--static
接口的成员(字段 + 方法)默认都是 public 的,并且不允许定义为 private 或者 protected。
从 Java 9 开始,允许将方法定义为 private,这样就能定义某些复用的代码又不会把方法暴露出去。
抽象类和接口比较:
1、一个类可以实现多个接口,但是不能继承多个抽象类。
2、接口的字段只能是 static 和 final 类型的,而抽象类的字段没有这种限制。
3、接口的成员只能是 public 的,而抽象类的成员可以有多种访问权限。
4、从设计层⾯来说,抽象是对类的抽象,是⼀种模板设计,⽽接⼝是对⾏为的抽象,是⼀种⾏为的规范。
使用接口:
需要让不相关的类都实现一个方法,例如不相关的类都可以实现 Comparable 接口中的 compareTo() 方法;
需要使用多重继承。
使用抽象类:
需要在几个相关的类中共享代码。
需要能控制继承来的成员的访问权限,而不是都为 public。
需要继承非静态和非常量字段。
20、覆写、重载
覆写(Override):存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法。
限制:–使用 @Override 注解,可以让编译器帮忙检查是否满足上面的三个限制条件。
1、子类方法的访问权限public必须大于等于父类方法protected;
2、子类方法的返回类型ArrayList<Integer>必须是父类方法返回类型或为其子类型 List<Integer>。
3、子类方法抛出的异常类型 Exception必须是父类抛出异常类型或为其子类型 Throwable。
方法调用的优先级为:
this.func(this)--------本类中是否有对应的方法
super.func(this)------父类中有没有对应方法
this.func(super)------对参数向上转型,超参
super.func(super)----查找父类中有没有这个超参
重载(Overload):
存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。
应该注意的是,返回值不同,其它都相同不算是重载。
21、JRE & JDK
JRE:
Java Runtime Environment, Java运行环境,为Java的运行提供所需环境。
是一个JVM程序,主要包括了JVM的标准实现和一些Java基本类库。
JDK:
Java Development Kit,Java开发工具包,提供了Java的开发及运行环境。
JDK是Java开发的核心,集成了JRE和一些其它的工具,比如编译Java源码的编译器javac等。
22、Java和C++的区别
1、Java 是纯粹的面向对象语言,所有的对象都继承自 java.lang.Object,C++ 为了兼容 C 即支持面向对象也支持面向过程。
2、Java 通过虚拟机从而实现跨平台特性,但是 C++ 依赖于特定的平台。
3、Java 没有指针,它的引用可以理解为安全指针,而 C++ 具有和 C 一样的指针。
4、Java 支持自动垃圾回收,而 C++ 需要手动回收。
5、Java 不支持多重继承,只能通过实现多个接口来达到相同目的,而 C++ 支持多重继承。
6、Java 不支持操作符重载,虽然可以对两个 String 对象执行加法运算,但是这是语言内置支持的操作,不属于操作符重载,而 C++ 可以。
7、Java 的 goto 是保留字,但是不可用,C++ 可以使用 goto。
23、Java 8 新特性:
1、Lambda 表达式 − Lambda 允许把函数作为一个方法的参数(函数作为参数传递到方法中)。
2、方法引用 − 方法引用提供了非常有用的语法,可以直接引用已有Java类或对象(实例)的方法或构造器。
与lambda联合使用,方法引用可以使语言的构造更紧凑简洁,减少冗余代码。
3、默认方法 − 默认方法就是一个在接口里面有了一个实现的方法。
4、新工具 − 新的编译工具,如:Nashorn引擎 jjs、 类依赖分析器jdeps。
5、Stream API −新添加的Stream API(java.util.stream) 把真正的函数式编程风格引入到Java中。
6、Date Time API − 加强对日期与时间的处理。
7、Optional 类 − Optional 类已经成为 Java 8 类库的一部分,用来解决空指针异常。
8、Nashorn, JavaScript 引擎 − Java 8提供了一个新的Nashorn javascript引擎,它允许我们在JVM上运行特定的javascript应用。
24、反射
每个类都有一个 Class 对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的 .class 文件,该文件内容保存着 Class 对象。
类加载相当于 Class 对象的加载,类在第一次使用时才动态加载到 JVM 中。也可以使用 Class.forName("com.mysql.jdbc.Driver") 这种方式来控制类的加载,该方法会返回一个 Class 对象。
反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class 不存在也可以加载进来。
Class 和 java.lang.reflect 一起对反射提供了支持,java.lang.reflect 类库主要包含了以下三个类:
Field :可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字段;
Method :可以使用 invoke() 方法调用与 Method 对象关联的方法;
Constructor :可以用 Constructor 的 newInstance() 创建新的对象。
反射的优点:
1、可扩展性 :应用程序可以利用全限定名创建可扩展对象的实例,来使用来自外部的用户自定义类。
2、类浏览器和可视化开发环境 :一个类浏览器需要可以枚举类的成员。可视化开发环境(如 IDE)可以从利用反射中可用的类型信息中受益,以帮助程序员编写正确的代码。
3、调试器和测试工具 : 调试器需要能够检查一个类里的私有成员。测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义,以确保一组测试中有较高的代码覆盖率。
反射的缺点:
尽管反射非常强大,但也不能滥用。如果一个功能可以不用反射完成,那么最好就不用。在我们使用反射技术时,下面几条内容应该牢记于心。
1、性能开销 :反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。
2、安全限制 :使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如 Applet,那么这就是个问题了。
3、内部暴露 :由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。
25、注解 Annotation
Java 注解是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。注解不会也不能影响代码的实际逻辑,仅仅起到辅助性的作用。包含在 java.lang.annotation 包中。
26、异常
1.所有的异常都是从Throwable继承而来的,是所有异常的共同祖先。
2.Throwable有两个子类,Error和Exception。 其中Error是错误,对于所有的编译时期的错误以及系统错误都是通过Error抛出的。这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。
3.Exception,是另外一个非常重要的异常子类。它规定的异常是程序本身可以处理的异常。异常和错误的区别是,异常是可以被处理的,而错误是没法处理的。
4.Checked Exception
可检查的异常,这是编码时非常常用的,所有checked exception都是需要在代码中处理的。它们的发生是可以预测的,正常的一种情况,可以合理的处理。比如IOException,或者一些自定义的异常。除了RuntimeException及其子类以外,都是checked exception。
5.Unchecked Exception
RuntimeException及其子类都是unchecked exception。比如NPE空指针异常,除数为0的算数异常ArithmeticException等等,这种异常是**运行时发生**,无法预先捕捉处理的。Error也是unchecked exception,也是无法预先处理的。
try
{
// 程序代码
}catch(ExceptionName e1)
{
//Catch 块
}
1. 通过try...catch语句块来处理:
Catch 语句包含要捕获异常类型的声明。当保护代码块中发生一个异常时,try 后面的 catch 块就会被检查。
如果发生的异常包含在 catch 块中,异常会被传递到该 catch 块,这和传递一个参数到方法是一样。
2. 另外,也可以在具体位置不处理,直接抛出,通过throws/throw到上层再进行处理。
具体的,如果一个方法没有捕获到一个检查性异常,那么该方法必须使用 throws 关键字来声明。
throws 关键字放在方法签名的尾部。
也可以使用 throw 关键字抛出一个异常,无论它是新实例化的还是刚捕获到的。
27、泛型
1、什么是泛型?为什么要使用泛型?
泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
在集合中存储对象并在使用前进行类型转换是多么的不方便。泛型防止了那种情况的发生。它提供了编译期的类型安全,确保你只能把正确类型的对象放入集合中,避免了在运行时出现ClassCastException。
2、 Java的泛型是如何工作的 ? 什么是类型擦除 ?
泛型是通过类型擦除来实现的,编译器在编译时擦除了所有类型相关的信息,所以在运行时不存在任何类型相关的信息。例如List在运行时仅用一个List来表示。这样做的目的,是确保能和Java 5之前的版本开发二进制类库进行兼容。你无法在运行时访问到类型参数,因为编译器已经把泛型类型转换成了原始类型。根据你对这个泛型问题的回答情况,你会得到一些后续提问,比如为什么泛型是由类型擦除来实现的或者给你展示一些会导致编译器出错的错误泛型代码。请阅读我的Java中泛型是如何工作的来了解更多信息。
Java的泛型是伪泛型。在编译期间,所有的泛型信息都会被擦除掉。正确理解泛型概念的首要前提是理解类型擦除(type erasure)。
泛型的实现是靠类型擦除技术 类型擦除是在编译期完成的 也就是在编译期 编译器会将泛型的类型参数都擦除成它的限定类型,如果没有则擦除为object类型之后在获取的时候再强制类型转换为对应的类型。 在运行期间并没有泛型的任何信息,因此也没有优化。
[10道泛型面试题](https://cloud.tencent.com/developer/article/1033693)
[泛型详解](https://www.cnblogs.com/Blue-Keroro/p/8875898.html)
28、Volatile关键字
为了解决线程并发的问题,在语言内部引入了 同步块 和 volatile 关键字机制。
synchronized同步块:
在多线程访问的时候,关键字synchronized可以保证同一时刻只能有一个线程执行某方法或某代码块。
volatile关键字:
多线程编程中,不希望对拷贝的副本进行操作,希望直接进行原始变量的操作(节约复制变量副本与同步的时间),就可以在变量声明时使用volatile关键字。
使用volatile定义的变量在进行操作时直接进行原始变量内容的处理。
被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。
volatile关键字作用:
Java内存模型有三大特性:
1、可见性:指线程间的可见性,一个线程修改的状态对另一个线程是可见的。
2、原子性:指具有不可分割性。---Java中synchronized和锁中的操作是保证原子性的。
非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。JUC中AtomicInteger、AtomicLong、AtomicReference
3、有序性:Java语言提供了volatile和synchronized两个关键字保证线程之间操作的有序性。
volatile是本身禁止指令重排序,synchronized是--一个变量在同一个时刻只允许一个线程对其进行lock操作。
1、保证变量的内存可见性
使用CPU总线嗅探机制告知其它线程该变量副本已失效,需要重新从主内存中获取。
相比 synchronized 而言,volatile 可以看作是一个轻量级锁,所以使用 volatile 的成本更低,因为它不会引起线程上下文的切换和调度。但 volatile 无法像 synchronized 一样保证操作的原子性。
2、不保证原子性
原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行。
在多线程环境下,volatile 关键字可以保证共享数据的可见性,但是并不能保证对数据操作的原子性。也就是说,多线程环境下,使用 volatile 修饰的变量是线程不安全的。
对任意单个使用 volatile 修饰的变量的读 / 写是具有原子性。
3、禁止指令重排序
为了提高性能,编译器和处理器通常会对指令进行重排序。重排序的顺序:
1、编译器优化重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
2、指令级并行重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
3、内存系统重排序。由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
为了实现volatile内存可见性,JMM会限制特定类型的编译器和处理器重排序。
Java 编译器在生成字节码时,会在指令序列中插入内存屏障指令来禁止特定类型的处理器重排序。
内存屏障是一组处理器指令,它的作用是禁止指令重排序和解决内存可见性的问题。
指令重排序时不能把后面的指令重排序到内存屏障之前的位置
Java内存模型- JMM 的可见性问题:
JMM 定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。
每个线程有自己的工作内存,保存共享变量的副本。
线程对变量的读写操作都是对自己的工作内存中操作,不能直接读写主内存中变量。
不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值传递需要通过主内存中转完成。
因此多线程下共享变量的可见性存在问题—解决:使用synchronized加锁、使用volatile关键字
1、加锁:进入synchronized同步代码块后,线程获得锁,会清空本地内存,然后从主内存中拷贝共享变量的最新值到本地内存作为副本。之后将修改后的副本值刷新到主内存,最后线程释放锁。
2、使用volatile关键字:使用volatile修饰共享变量,在线程操作变量副本并写会主内存后,通过CPU总线嗅探机制告知其它线程该变量副本已失效,需要重新从主内存中获取。
总线嗅探机制:–实现缓存一致性
计算机中为了提高处理速度,CPU不直接与内存进行通信,而是在CPU与内存之间加入许多寄存器、多级缓存,解决CPU运算速度和内存读取速度不一致的问题。
由于 CPU 与内存之间加入了缓存,在进行数据操作时,先将数据从内存拷贝到缓存中,CPU 直接操作的是缓存中的数据。但在多处理器下,将可能导致各自的缓存数据不一致(这也是可见性问题的由来),为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,而嗅探是实现缓存一致性的常见机制。
缓存一致性问题不是由多CPU造成,而是多缓存导致的。
嗅探机制工作原理:
每个处理器通过监听在总线上传播的数据来检查自己的缓存值是不是过期了,如果处理器发现自己缓存行对应的内存地址修改,就会从将当前处理器的缓存行设置无效状态,当处理器对这个数据进行修改操作时,会重新从主内存中把数据读到CPU缓存中。