众所周知,为了保证某个对象在各个场景中使用的唯一性,常用的一种设计模式就是单例模式。我们最熟悉的单例模式饿汉模式如下:
public class SingleInstance {
private static final SingleInstance instance = new SingleInstance();
private SingleInstance() {
}
public static SingleInstance getInstance() {
return instance;
}
}
它的优势在于,在高并发程序的任何请求中,我们都可以唯一的使用这个对象,而不会创建出新的对象来。其中在创建singleInstance这个对象的时候,分别使用了private,static,final这三个关键词。private的作用显而易见,保证了对象只在此类中能够使用,同时也封装了singleInstance类的构造方法,保证不会再其他地方创建出新的对象。那么static和final关键词再这里起了什么作用呢?下面一探究竟。
1. static
static是什么?
static是java中的一个关键字,有详细的static关键词的概念说明如下:
- A static method belongs to the class rather than the object of a class.
- A static method can be invoked without the need for creating an instance of a class.
- A static method can access static data member and can change the value of it.
这样看可能不是特别直观,直接使用一个程序来说明:
public class ClassLazyInit {
public static void main(String[] args) {
System.out.println(Collection.class.hashCode());
System.out.println(Collection.flag);
}
static class Collection{
static boolean flag=true;
static{
System.out.println("Collection initializing...");
}
}
}
1808253012
Collection init...
true
Process finished with exit code 0
从运行结果可以看到,当执行类的hashCode()方法时候,Collection中的static块并没有被初始化,当主函数中调用了Collection中的某个静态变量,这时候这个类才被初始化。
static的特殊含义
static可以保证在一个线程未使用其他同步机制的情况下总是可以读到一个类的静态变量的初始值。
很好理解的是,static变量是随着类被初次访问而初始化的。在多线程的环境中,需要保证一个变量在线程中的可见性是需要对内存做一些操作指令的。可以简易来描述下这个过程,当两个线程A,B对一个共享变量进行操作的时候,首先是把内存中的数据加载进各自的处理器中,然后在放入各自的寄存器。当A中更新共享变量的时候,首先会被放入写缓存器中,然后再写入高速缓存中,最后一步是放进内存中,每个处理器都有各自的写缓存器和高速缓存。所以在一个时间点,线程B读取的共享变量值并不是A更新的那一个,很可能是一个旧值。在计算机系统设计中,为了保证变量的可见性,有一种协议叫做缓存一致性协议,它的大概作用就是说,不同的处理器可以读取对方高速缓存中的值。这时候我们要保证可见性只需要一步,就是当共享变量被更新的时候,原子性保证把值写入高速缓存中就可以了,volatile的实现方式也是基于这种思想。那么static变量所做的事情就是,在某个线程调用的时候,就写入内存中这个值,保证内存中必然有这个值的存在,这里注意:static并不能保证多线程之后操作的可见性。
2. final
Java final variable
If you make any variable as final, you cannot change the value of final variable(It will be constant).
我们都知道,final修饰的变量值不会改变。但是在多线程的环境中,它还会保证两点,1. 其他线程所看到的final字段必然是初始化完毕的。 2. final修饰的变量不会被程序重排序。
下面举例说明
public class HttpRequest{
private String id;
private final Request request;
public HttpRequest(String id, String name, String age){
this.id=id;
this.request=new Reqeust(String name, String age);
}
static class Request{
private String name;
private String age;
public Request(String name, String age){
this.name=name;
this.age=age;
}
}
}
之上是一段很简单的初始化程序,当我们调用它的时候,它的编译如下:
http=new HttpRequest("200","wang","22");
----------------------------------
obj=allocate(HttpRequest.class);
obj.id="200";
objRequest=allocate(Range.class);
objRequest.name="wang";
objRequest.age="22";
obj.request=objRequest;
http=obj;
可以保证的是,objRequest的任何赋值操作都在被它的父类引用之前。因为加了final关键字,所以可以严格的保证之上的顺序。
3. 总结
我们回顾下,static保证了变量的初始值,final保证了不被JIT编译器重排序。对于一个单例模式来说,它所在的类在被引用的时候,static会保证它被初始化完毕,且是所有线程所见的初始化,final保证了实例初始化过程的顺寻性。两者结合保证了这个实例创建的唯一性。