(总结)Java相关_关键字总结
final、volatile、this、super、static
1.final
final关键字可以用于三个地方:
- 用于修饰类
- 被final关键字修饰的类不能被继承;
- final类中的所有成员方法都会被隐式的指定为final方法;
- 类属性
- 被final关键字修饰的类属性,子类就不能重新赋值;
- 对于一个final变量如果是基本数据类型变量,数值一旦在初始化之后就不能更改,如果是引用类型变量,则在对其初始化之后便不能让其再指向另一个变量(注意的是:它指向的对象的内容是可变的);
- final变量定义的时候,可以先声明,而不给初值,在使用之前必须被初始化(可以直接赋值,也可以在实例构造器中赋值)
- 类方法。
- 被final关键字修饰类方法不能被覆盖(重写),将方法锁定,防止任何继承类篡改;
final变量和普通变量区别:
public class Test {
public static void main(String[] args) {
String a = "helloWorld";
final String b = "hello";
String c = "hello";
String d = b + "World"; //相当于 c = "hello" + 2;会自动优化
String e = d + "World";
System.out.println((a == d));
System.out.println((a == e));
}
}
结果:
true
false
当final变量是基本数据类型以及String类型时,如果在编译期间能知道它的确切值,则编译器会把它当做编译期常量使用。
public class Test {
public static void main(String[] args) {
Son s1 = new Son();
System.out.println(s1.name);
}
}
class Father{
final String name = "jack";
}
class Son extends Father{
String name = "bob";
}
结果:
bob
请注意,我们在Son类name之前加上了一个String关键字,其结果是从新发定义了一个变量,虽然变量名都是name,但是在内存中占用的区域却不同,所以不与之冲突,不会报错。
2.volatile
①保证内存可见性
- 基本概念
- 可见性是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果,另一个线程马上就能看到。
- 实现原理
- 当对非volatile变量进行读写的时候,每个线程先从主内存拷贝变量到CPU缓存中,如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的CPU cache中。
- volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,保证了每次读写变量都从主内存中读,跳过CPU cache这一步。 当一个线程修改了这个变量的值,新值对于其他线程是立即得知的。
②禁止指令重排
- 基本概念
- 指令重排序是JVM为了优化指令、提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。指令重排序包括编译器重排序和运行时重排序。
- 在JDK1.5之后,可以使用volatile变量禁止指令重排序。针对volatile修饰的变量,在读写操作指令前后会插入内存屏障,指令重排序时不能把后面的指令重排序。
- 示例说明:
double r = 2.1; //(1)
double pi = 3.14;//(2)
double area = pi*r*r;//(3)
虽然代码语句的定义顺序为1->2->3,但是计算顺序1->2->3与2->1->3对结果并无影响,所以编译时和运行时可以根据需要对1、2语句进行重排序。
- 指令重排带来的问题
如果一个操作不是原子的,就会给JVM留下重排的机会。
线程A中
{
context = loadContext();
inited = true;
}
线程B中
{
if (inited)
fun(context);
}
如果线程A中的指令发生了重排序,那么B中很可能就会拿到一个尚未初始化或尚未初始化完成的context,从而引发程序错误。
- 禁止指令重排的原理
- volatile关键字提供内存屏障的方式来防止指令被重排,编译器在生成字节码文件时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
- JVM内存屏障插入策略:
- 每个volatile写操作的前面插入一个StoreStore屏障;
- 在每个volatile写操作的后面插入一个StoreLoad屏障;
- 在每个volatile读操作的后面插入一个LoadLoad屏障;
- 在每个volatile读操作的后面插入一个LoadStore屏障。
- 指令重排在双重锁定单例模式中的影响
基于双重检验的单例模式(懒汉型)
public class Singleton3 {
private static Singleton3 instance = null;
private Singleton3() {}
public static Singleton3 getInstance() {
if (instance == null) {
synchronized(Singleton3.class) {
if (instance == null)
instance = new Singleton3();// 非原子操作
}
}
return instance;
}
}
instance= new Singleton()并不是一个原子操作,其实际上可以抽象为下面几条JVM指令:
memory =allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance =memory; //3:设置instance指向刚分配的内存地址
上面操作2依赖于操作1,但是操作3并不依赖于操作2。所以JVM是可以针对它们进行指令的优化重排序的,经过重排序后如下:
memory =allocate(); //1:分配对象的内存空间
instance =memory; //3:instance指向刚分配的内存地址,此时对象还未初始化
ctorInstance(memory); //2:初始化对象
指令重排之后,instance指向分配好的内存放在了前面,而这段内存的初始化被排在了后面。在线程A执行这段赋值语句,在初始化分配对象之前就已经将其赋值给instance引用,恰好另一个线程进入方法判断instance引用不为null,然后就将其返回使用,导致出错。
解决办法:
用volatile关键字修饰instance变量,使得instance在读、写操作前后都会插入内存屏障,避免重排序。
public class Singleton3 {
private static volatile Singleton3 instance = null;
private Singleton3() {}
public static Singleton3 getInstance() {
if (instance == null) {
synchronized(Singleton3.class) {
if (instance == null)
instance = new Singleton3();
}
}
return instance;
}
}
③适用场景
- volatile是轻量级同步机制。在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,是一种比synchronized关键字更轻量级的同步机制。
- volatile无法同时保证内存可见性和原子性。加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。
- volatile不能修饰写入操作依赖当前值的变量。声明为volatile的简单变量如果当前值与该变量以前的值相关,那么volatile关键字不起作用,也就是说如下的表达式都不是原子操作:“count++”、“count = count+1”。
- 当要访问的变量已在synchronized代码块中,或者为常量时,没必要使用volatile;
- volatile屏蔽掉了JVM中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。
④为什么不能保证原子性
1、线程读取i 2、temp = i + 1 3、i = temp 当 i=5 的时候A,B两个线程同时读入了 i 的值, 然后A线程执行了 temp = i + 1的操作, 要注意,此时的 i 的值还没有变化,然后B线程也执行了 temp = i + 1的操作,注意,此时A,B两个线程保存的 i 的值都是5,temp 的值都是6, 然后A线程执行了 i = temp (6)的操作,此时i的值会立即刷新到主存并通知其他线程保存的 i 值失效, 此时B线程需要重新读取 i 的值那么此时B线程保存的 i 就是6,同时B线程保存的 temp 还仍然是6, 然后B线程执行 i=temp (6),所以导致了计算结果比预期少了1
主要原因在于 i++ 这个操作本身不是原子性的(可以分为读取 i,执行 i+1 操作,把 i+1 赋值给 i 这三个操作),如果两个线程A和B,他们都读取了主存中的 i 值,A进行了 i+1 的操作,之后被阻塞,B又进行了 i+1 的操作,之后将新值赋值给 i,i 值被刷新回主存,此时由于A已经执行完了 i+1 操作,所以即使主存中的i值改变了,也不会读取新值;其他评论中已经读取过i的值不会再次读取、存在寄存器之中的说法感觉还是不对的,重要的点在于是否执行了 i+1 操作,执行了该操作之后则不会重新读取 i 值,如果只是读取了 i 值,在进行下一步操作之前,主存中的 i 已经变化了,那么我觉得还是会在缓存一致性原则下,刷新已经读取过的值。
3.this
- 1.调用属性,区分成员变量和局部变量;
- 2.调用方法(普通方法与构造方法), 必须放在构造方法首行;
- 3.当前对象调用:
class BlueMoon {
public void print() {
//哪个对象调用了print()方法,this就自动与此对象指向同一块内存地址
System.out.println("this=" + this);//this 就是当前调用对象
}
}
public class thisDemo02 {
public static void main(String[] args) throws Exception {
BlueMoon bm = new BlueMoon();
BlueMoon bm2 = new BlueMoon();
System.out.println("bm=" + bm);
bm.print();
System.out.println("---------------------");
System.out.println("bm2=" + bm2);
bm.print();
}
}
result:
bm=keywordsTest.BlueMoon@15db9742
this=keywordsTest.BlueMoon@15db9742
---------------------
bm2=keywordsTest.BlueMoon@6d06d69c
this=keywordsTest.BlueMoon@15db9742
4.super
super关键字用于从子类访问父类的变量和方法。 例如:
public class Super {
protected int number;
protected showNumber() {
System.out.println("number = " + number);
}
}
public class Sub extends Super {
void bar() {
super.number = 10;
super.showNumber();
}
}
在上面的例子中,Sub 类访问父类成员变量 number 并调用其其父类 Super 的 showNumber() 方法。
使用 this 和 super 要注意的问题:
- 在构造器中使用 super() 调用父类中的其他构造方法时,该语句必须处于构造器的首行,否则编译器会报错。另外,this 调用本类中的其他构造方法时,也要放在首行。
- this、super不能用在static方法中。
5.static
- 修饰成员变量和成员方法(静态变量,静态方法)
- 静态代码块
- 修饰类(只能修饰内部类,静态内部类)
- 静态导包(用来导入类中的静态资源,1.5之后的新特性)
①静态变量
静态变量和非静态变量的区别:
- 静态变量被所有对象共享, 在内存中只有一个副本,在类初次加载的时候才会初始化,所以单例模式中饿汉式天生线程安全;
- 非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响;
②静态方法
- static修饰的成员变量和方法,从属于类,可以方便在没有创建对象的情况下进行调用(方法/变量),只要类被加载了,就可以通过类名去进行访问;
- 在静态方法中不能访问类的非静态成员变量和非静态方法,因为非静态成员变量和非静态方法都必须依赖于具体的对象才能被调用;
- 虽然在静态方法中不能访问非静态成员方法和非静态成员变量,但是在非静态成员方法中是可以访问静态成员方法和静态成员变量(为类中的信息);
- static方法是属于类的,非实例对象,在JVM加载类时,就已经存在内存中,不会被虚拟机GC回收掉,这样内存负荷会很大,但是非static方法会在运行完毕后被虚拟机GC掉,减轻内存压力;
②静态代码块
- 静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—非静态代码块—构造方法)。 该类不管创建多少对象,静态代码块只执行一次。
- 很多时候会将一些只需要进行一次的初始化操作都放在static代码块中进行
- 静态初始化块可以置于类中的任何地方,类中可以有多个静态初始化块。
在类初次被加载时,会按照静态初始化块的顺序来执行每个块,并且只会执行一次。
静态代码块的格式是:
static {
语句体;
}
一个类中的静态代码块可以有多个,位置可以随便放,它不在任何的方法体内,JVM加载类时会执行这些静态的代码块,如果静态代码块有多个,JVM将按照它们在类中出现的先后顺序依次执行它们,每个代码块只会被执行一次。
静态代码块对于定义在它之后的静态变量,可以赋值,但是不能访问。