概述
我们来看下一个类如何才能只被实例化一次。我们知道一般实例化对象时,都会调用类的构造方法。如果构造方法为public,那么肯定每个人都能实例化这个类,也就无法保证该类对象的唯一性。所以这个类的构造方法必须为private,不能向外界提供。但是这样我们就无法调用它的构造方法,也就无法实例化对象了,那么由谁来实例化呢?想必你也想到了,由类自身调用,因为这时候也只有它自身能调用构造方法了
实现
饿汉式
public class Singleton {
public static Singleton singleton = new Singleton();
private Singleton(){
System.out.println("singleton init");
}
public static Singleton getInstance(){
return singleton;
}
//测试
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2);
}
}
output:
singleton init
true
缺点:类加载完就会创建实例对象。
懒汉式(懒加载)
public class Singleton {
public static Singleton singleton = null;
private Singleton(){
System.out.println("singleton init");
}
public static Singleton getInstance(){
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
//测试
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2);
}
}
output:
singleton init
true
线程安全的懒汉式
//测试
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
Singleton.getInstance();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
Singleton.getInstance();
}
});
t1.start();
t2.start();
}
output:
singleton init
singleton init
解决方法一(同步方法)
public synchronized static Singleton getInstance(){
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
用synchronized修饰这个方法,相当于给这个方法加了把锁,只要有线程进入到这个方法里面,那么这个锁就会被锁上,这时其他的线程想要执行这个方法时,一看,呦,厕所门关着待会再来上。只有当里面的线程执行完这个方法后,这个锁才会打开,其他线程才能进入。这样就很好的避免前面重复创建对象的问题。synchronized虽然解决了这个问题,但是synchronized会降低程序执行效率。
解决方法二(同步代码块)
public static Singleton getInstance(){
if(singleton == null){
synchronized (Singleton.class) {
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
- 同步代码块synchronized 后面的括号中需要一个对象,可以任意,这里我们用了Singleton的类对象Singleton.class
- 方法中进行了两次对象是否为空的判断,一次在同步代码块外面,一次在里面。因此称之为双重检验锁模式(Double Checked Locking Pattern)
- 第一个if判断,如果没有实例化则加锁
现在我们的双重检验锁模式既解决了在多线程中重复创建对象问题,又提高了代码执行效率,同时还是懒加载模式,是不是已经非常完美了?
问题出现在:
singleton = new Singleton();
在jvm中执行该对象的创建语句需要三步:
- 在栈内存中创建singleton 变量,在堆内存中开辟一个空间用来存放Singleton实例对象,该空间会得到一个随机地址,假设为0x0001
- 对Singleton实例对象初始化
- 将singleton变量指向该对象,也就是将该对象的地址0x0001赋值给singleton变量,此时singleton就不为null了
程序的运行其实就是CPU在一条条执行指令,有的时候CPU为了提高程序运行效率会打乱部分指令的执行顺序,也就是所谓的指令重排序
当CPU执行时,是无法保证一定是按照123的顺序执行,也可能由于指令重排序的优化,会以132的顺序执行
假设现在有两个线程A、B,CPU先切换到线程A,当执行上述创建对象语句时,假设是以132的顺序执行,当线程A执行完3时(执行完第3步后singleton就不为null了),突然停住了,CPU切换到了线程B去调用getInstance方法,由于singleton此时不为null,就直接返回了singleton,但此时步骤2是还没执行的,返回的对象还是未初始化的,这样程序也就出问题了。那有什么方法解决吗?很简单只要用volatile修饰singleton变量就可以了。
public static volatile Singleton singleton = null;
- volatile可以禁止指令重排
- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
两种比较简洁的单例模式,即用静态内部类和枚举来实现
静态内部类
private Singleton(){
System.out.println("singleton init");
}
private static class SingletonHolder{
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.INSTANCE;
}
可以看到,我们在Singleton 类内部定义一个SingletonHolder 静态内部类,里面有一个对象实例,当然由于Java虚拟机自身的特性,只有调用该静态内部类时才会创建该对象,从而实现了单例的延迟加载,同样由于虚拟机的特性,该模式是线程安全的,并且后续读取该单例对象时不会进行同步加锁的操作,提高了性能。
枚举实现
枚举使用
public enum Person {
person1;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
//测试
Person.person1.setName("jaixinxiao");
System.out.println(Person.person1.getName());
output:
jiaxinxiao
person1就是我们这个枚举类的一个对象实例,也就是说,如果你要获取一个Person对象,不用再像上面那样调用new Person()来创建对象,直接获取这个枚举类中的person1就可以了。
这样你就只能获取到一个Person实例对象了。可能有的人有疑惑了,不对啊,难道我就不能再new一个吗?这个是不能的,因为枚举类的构造方法是私有掉的,你是无法调用到的并且你也无法通过反射来创建该实例,这也是枚举的独特之处。
如果这个Person的name需要在对象创建时就初始化好,那该怎么办呢?很简单,就和普通类一样只要在里面定义一个构造方法,传入name参数就可以了
public enum Person {
person1("张三");
Person(String name){
this.name = name;
}
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
可以看到就和普通类一样,我们在枚举类中定义了一个入参为name的构造方法,注意这构造方法前面虽然没有加权限修饰的,但并不表示它的权限是默认的,前面提到枚举类中的构造方法是私有的,即使你强行给它加个public,编辑器也会报错。
枚举类实例的创建也是线程安全的,所以使用枚举来实现单例也是一种比较推荐的方法,但是如果你是做Android开发的,你需要注意了,因为使用枚举所使用的内存是静态变量的两倍之上,如果你的应用中使用了大量的枚举,这将会影响到你应用的性能,不过一般用的不多的话还是没什么关系的。