在平时编码中,我们可能只注意了这些static,final,volatile等关键字的使用,忽略了他们的细节,更深层次的意义。
本文总结了Java中所有常见的关键字以及一些例子。
static 关键字
概述:
当static修饰类的属性或者方法时,那么就可以在没有创建对象的情况下使用该属性或方法。
静态块也是static的一个应用,用于初始化类时的一些操作。
静态方法和静态变量
划重点
被static修饰后的属性或者方法,使用时不需要new 一个类,用类.属性名或方法名访问.
比如java.lang.Math就存放了很多静态资源,可以直接使用Math.random()来获取随机数.
一些需要注意的地方
非静态方法是可以访问静态资源的,
静态方法是不能引用非静态资源的。
来看一个代码实例:
1 public class TestStatic { 2 3 protected int i = 100; 4 5 public static void main(String args[]){ 6 System.out.println(i); 7 } 8 }
在以上代码,编译的时候会出错,main方法是静态方法,变量i是非静态的。
解决办法是,将变量i加上static修饰。
不经就要提出一个问题,
为什么非静态方法可以访问静态资源,而静态方法不能访问非静态资源呢?
从类加载机制上讲,静态资源是类初始化的时候加载的,然后非静态资源是new一个该类的对象的时候加载的。
这就带来一个问题:
加载类时默认先加载静态资源的,当new一个对象之后,才会加载其他资源,所以在new对象之前,静态资源是不知道类有哪些非静态资源的,
但是当对象new出来之后,该类的所有属性和方法都知道。
还有需要注意的是:
1.静态属性和方法可以通过类.属性名或方法名,而且,该类的对象也是访问静态属性和变量的。
2.Java的语法规定,static不能修饰局部变量。没有为什么,这就是规定。
静态块
静态块和静态变量、静态方法是没什么区别的,也是在类加载的时候执行,而且只执行一次。
关于静态块有两点需要注意:
1.静态资源的加载顺序严格按照静态资源的定义顺序加载的
2.静态块,对于定义在它之后的静态变量,可以赋值但不能访问。
static的题目
下面main()方法的输出结果是什么:
public class InstanceClass extends ParentClass{ public static String subStaticField = "子类静态变量"; public String subField = "子类非静态变量"; public static StaticClass staticClass = new StaticClass("子类"); static { System.out.println("子类 静态块初始化"); } { System.out.println("子类 [非]静态块初始化"); } public InstanceClass(){ System.out.println("子类构造器初始化"); } public static void main(String args[]) throws InterruptedException { new InstanceClass(); } } class ParentClass{ public static String parentStaticField = "父类静态变量"; public String parentField = "父类[非]静态变量"; public static StaticClass staticClass = new StaticClass("父类"); static { System.out.println("父类 静态块初始化"); } { System.out.println("父类 [非]静态块初始化"); } public ParentClass(){ System.out.println("父类 构造器初始化"); } } class StaticClass{ public StaticClass(String name){ System.out.println(name+" 静态变量加载"); } }
输出结果:
下面是我总结类加载流程,可以对照着这个流程,可以再重新看一下上面的例子,会有新的理解。
1. 加载父类静态
1.1 为静态属性分配存储空间并赋初始值
1.2 执行静态初始化块和静态初始化语句(从上至下)
2. 加载子类静态
2.1 为静态属性分配存储空间
2.2 执行静态初始化块和静态初始化语句(从上至下)
3. 加载父类非静态
3.1 为非静态块分配空间
3.2 执行非静态块
4. 加载子类非静态
4.1 为非静态块分配空间
4.2 执行非静态块
5. 加载父类构造器
5.1 为实例属性分配存数空间并赋初始值
5.2 执行实例初始化块和实例初始化语句
5.3 执行构造器内容
6. 加载子类构造器
6.1 为实例属性分配存数空间并赋初始值
6.2 执行实例初始化块和实例初始化语句
6.3 执行构造器内容
对照着刚才的规则,再看一下这个例子:
1 public class TestStaticLoad { 2 Person person = new Person("TestStaticLoad"); 3 static{ 4 System.out.println("TestStaticLoad static"); 5 } 6 7 public TestStaticLoad() { 8 System.out.println("TestStaticLoad constructor"); 9 } 10 11 public static void main(String[] args) { 12 new God(); 13 } 14 15 } 16 17 class Person{ 18 static{ 19 System.out.println("person static"); 20 } 21 public Person(String str) { 22 System.out.println("person "+str); 23 } 24 } 25 26 27 class God extends TestStaticLoad { 28 Person person = new Person("God"); 29 static{ 30 System.out.println("God static"); 31 } 32 33 public God() { 34 System.out.println("God constructor"); 35 } 36 }
输出结果:
一步一步地解析:
- 在TestStaticLoad 的main方法中,执行了new God(),那就就会去加载God类,在这之前会先加载它的父类:TestStaticLoad
- 第一步:加载父类静态,执行System.out.println("TestStaticLoad static"); 输出:TestStaticLoad static,
- 第二步:加载子类静态,执行System.out.println("God static");,输出God static
- 第三步:加载父类非静态,Person person = new Person("TestStaticLoad");,这里实例化了Person 对象,那就会去加载Person类。
- 第四步:加载Person类,首先看有没有父类,没有。好,加载静态块,执行System.out.println("person static");输出person static
- 第五步:Pernson类静态块加载完毕,加载构造器,new一个Person对象,输出person TestStaticLoad。这时TestStaticLoad 类非静态块加载完毕
- 第六步:加载God 父类(TestStaticLoad )构造器,输出TestStaticLoad constructor
- 第七步:God父类全部加载完毕,加载God的非静态块,Person person = new Person("God");这时又会去加载Person类,需要注意的是,static块只加载一次,因为之前在父类已经加载过了,这时只加载构造器,输出person God
- 最后一步:加载本类God 的构造器,输出God constructor。
static关键字的总结:
- static关键字 可以再没有创建对象的时候进行调用类的元素
- static 可以修饰类的方法 以及类的变量, 以及静态代码块
- 被static修饰的成为静态方法,静态方法是没有this的,静态方法不能访问同一个类中的非静态方法和静态变量,但是非静态方法 可以可以访问静态变量
- 类的构造器 也是静态的
- 静态变量被所有的内存所有的对象共享,在内存中只有一个副本。非静态变量是是在创建对象的时候初始化的,存在多个副本,每个副本不受影响。
- static 静态代码块,static 代码块可以放在类中的任何地方,类加载的时候会按照static代码块的顺序来加载代码块,并且只会执行一次。
- 枚举类和静态代码块 赋值静态代码块的变量
- 非静态方法能够通过this访问静态变量
- 静态成员变量虽然独立于对象,但是不代表不可以通过对象去访问,所有的静态方法和静态变量都可以通过对象访问。
- static不可以修饰局部变量(java语法规定)
没想到static能有这么多需要注意的,可以说Java中的语法还是有很多可以深究的.
final 关键字
概述:
final关键字,在平时的过程中也是很常见的,在这里进行一下深入的学习,加深对final关键字的理解。
使用注意点:
1.在java中final可以用来修饰类、方法、和变量(包括成员变量和局部变量)
2.final修饰类的时候,这个类将永远不会被继承,类中的成员方法也会被隐式的修饰为final(尽量不要用final修饰类)
3.如果不想方法被继承,可以用final修饰,private也会隐式的将方法指定为final
4.final修饰变量的时候,如果是基本类型的变量,那么他的值在初始化之后就不能更改
5.final在修饰对象的时候,在其初始化之后就不能指向其他对象
6.被static和final修饰的变量,将会占据一段不能改变的存储空间,将会被看做编译期常量
7.不可变的是变量的引用而非引用指向对象的内容。
几个例子:
1.final变量和普通变量的区别
public class TestFinal { public static void main(String args[]){ String a = "test1"; final String b = "test"; String d = "test"; String c = b + 1; String e = d + 1; System.out.println((a == c)); System.out.println((a.equals(e))); } }
因为final变量是基本类型以及String时,在编译期的时候就把它当做常量来使用,不需要在运行时候使用。“==”是对比两个对象基于内存引用,如果两个对象的引用完全相同,则返回true,所以这里b是用访问常量的方式去访问,d是链接的方式,所以a的内存引用和c的内存引用是相等的,所以结果为true,a和e两个对象的值是相等的,所以结果为true
2.final在修饰对象的时候
1 public class TestFinal { 2 public static void main(String args[]){ 3 final TestFinal obj1 = new TestFinal(); 4 final TestFinal obj2 = new TestFinal(); 5 6 obj1 = obj2; 7 } 8 }
在编译的时候,或报错, 不能指向一个final对象。
volatile关键字
缓存一致性:
首先来看看线程的内存模型图:
当执行代码:
i = i + 1;
- 首先从主存中读取i的值,
- 然后复制I到Cache中,
- CPU执行指令对i进行加1
- 将加1后的值写入到Cache中
- 最后将Cache中i的值刷新到主存中
这个在单线程的环境中是没有问题的,但是运行到多线程中就存在问题了。
问题出在主存中的变量,因为有可能其他线程读的值,线程的Cache还没有同步到主存中,每个线程中的Cahe中的值副本不一样,可能会造成"脏读"。
缓存一致性协议解决了这样的问题,它规定每个线程中的Cache使用的共享变量副本是一样的。
核心内容是当CPU写数据时,如果发现操作的变量式共享变量,它将通知其他CPU该变量的缓存行为无效,
所以当其他CPU需要读取这个变量的时候,发现自己的缓存行为无效,那么就会从主存中重新获取。
三个概念
Jvm定义了内存规范,试图做到各个平台对内存访问的差异,但是依旧会发生缓存一致性的问题。
首先了解三个概念,原子性,可见性,有序性。
原子性:指某个操作,一个或者多个,要么全部执行并且执行的过程中不会被任何因素打断,要么都不执行。
在JVM中,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。看一个例子:
x = 70; //语句1
y = x; //语句2
y++; //语句3 y = x + 1; //语句4
上面四个语句中,只有语句1是原子性,其他都不是。
可见性:当多个线程访问一个变量时,一个线程修改了这个变量的值,其他线程能够看得到。
未加volatile变量修饰的变量,在被修改之后,什么时候写入到主存是不确定的,因此其他线程读取该变量的值可能还是未被修改的值。
如果改变了被volatile关键字修饰了,那么JVM将会标记它为共享变量,共享变量一经修改,就会立即同步到主存中,并且通知其他线程(CPU缓存)中值生效,请去主存中读取该值。
有序性:程序的执行顺序按照代码的先后顺序执行。但是JVM在执行语句的过程会对代码进行重排序(重排序:CPU为了提高程序运行效率,可能会对输入代码进行优化,但是不保证程序的执行先后顺序和代码中的顺序一致,但是会保证程序最终执行结果和代码顺序执行的结果是一致的)。
在多线程的环境下,原有的顺序执行会发生错误。
在JVM中保证了一定的有序性,比如被volatile修饰后的变量,那么该变量的写操作先行发生于后面对这个变量的读操作。
所以要想程序在多线程环境下正确运行,必须保证原子性,可见性,有序性。
volatile的作用
当一个变量(类的普通变量,静态变量)被volatile修饰之后,那么将具备两个属性:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序
下面来看看线程池中一些变量的定义:
private volatile ThreadFactory threadFactory;
private volatile RejectedExecutionHandler handler; private volatile long keepAliveTime; private volatile boolean allowCoreThreadTimeOut; private volatile int corePoolSize; private volatile int maximumPoolSize;
可以看到线程工厂threadFactory,拒绝策略handler,没有任务时的活跃时间keepAliveTime,keepAliveTime的开关allowCoreThreadTimeOut,核心池大小corePoolSize,最大线程数maximumPoolSize
都是被volatile修饰中,因为在线程池中有若干个线程,这些变量必需保持对线程可见性,不然会引起线程池运行不正确。
volatile不能保证原子性
i++;
它是非原子性的,当变量i被volatile修饰时,是否能保证原子性呢?
做个试验:
public class TestAtomVolatile {
public volatile int i = 0; public void increase() { i++; } public static void main(String[] args) throws InterruptedException { final TestAtomVolatile test = new TestAtomVolatile(); for(int i=0;i<10;i++){ new Thread(){ public void run() { for(int j=0;j<1000;j++) test.increase(); System.out.println(test.i); }; }.start(); } } }
以上代码就是10个线程,分别对变量i进行自增操作,预期结果应该是10000,但是总会存在着小于10000的情况。输出结果如下:
对于这种情况,可以使用锁,synchronize,Lock,也可以使用原子变量。
原子变量的例子:
volatile的原理
下面这段话摘自《深入理解Java虚拟机》:
“”观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
assert关键字
assert断言
在目前的java编码中,是不推荐使用的,这里只是稍微了解一下:
使用方式:
public class LearnAssert { public static void main(String args[]){ assert true; System.out.println("断言1成功执行"); System.out.println("-----------"); assert false:"error"; System.out.println("断言2成功执行"); } }
assert是为了在调试程序时候使用的,默认不推荐使用,测试程序可以使用junit。
synchronized关键字
关于锁关键字,有以下几个总结:
- 无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
- 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
- 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
下面介绍一个锁的实例:
public class ManyThread { int count = 0; public synchronized void autoIncrement() { count++; } public static void main(String args[]) { ManyThread manyThread = new ManyThread(); Runnable runnable = new MyRunnable2(manyThread); new Thread(runnable, "a").start(); new Thread(runnable, "b").start(); new Thread(runnable, "c").start(); new Thread(runnable, "d").start(); } } class MyRunnable2 implements Runnable { private ManyThread manyThread; public MyRunnable2(ManyThread manyThread) { this.manyThread = manyThread; } @Override public void run() { for (int i = 0; i < 10000; i++) { manyThread.autoIncrement(); System.out.println(Thread.currentThread().getName() + " 执行中 " + "count:" + manyThread.count); } } }
用synchronized修饰后的autoIncrement()方法,会被加锁,确保它每次执行的时候都能保证只有一个线程在运行。
transient关键字
Java中,一个类想要序列化,可以通过实现Serilizable接口的方式来实现,实现该接口之后,该类所有属性和方法都会自动序列化。
但是如果属性或方法被transient修饰,那么将不会被序列化。