首先要知道单例模式为何会出现,又或者说什么样的类可以做成单例模式
所有使用单例模式的类都有一个共性,那就是这个类没有自己的状态,换句话说,这些类无论你实际化多少个,其实都是一样的,而更重要的一点是,这个类有两个或者两个以上的实例的话,程序竟然会产生程序错误或者与现实违背的逻辑错误。
这样的话,如果我们不将这个类控制成单例的结构,应用中就会存在很多一模一样的类实例,这会非常浪费系统的内存资源,而且容易导致错误,单例模式就是为了尽可能的节约内存空间,减少无谓的GC消耗,并且使应用可以正常运作。
一般一个类能否做成单例模式,最容易区别的地方就在于,这些类在应用如果有两个或者两个以上的实例会引起错误,又或者就是这些类在整个应用中,同一时刻有且只能有一种状态。
第一种,最标准最原始的线程不安全单例模式构造方法:
public class SingleTon {
//一个静态实例
private static SingleTon singleTon;
//私有化构造器
private SingleTon(){};
//给出一个公共的静态方法返回一个单一实例
public static SingleTon getSingleTon(){
if (singleTon == null) {
singleTon = new SingleTon();
}
return singleTon;
}
}
这是在不考虑并发访问的情况下标准的单例模式的构造方法,这种方法通过几个地方限制我们取到的实例是唯一的
1.静态实例,带有static关键字的属性在每一个类中都是唯一的。
2.限制客户端随意创造实例,即私有化构造方法,此为保证单例的最重要的一步。
3.给一个公共的获取实例的静态方法,注意,是静态的方法,因为这个方法是在我们未获取到实例的时候就要提供给客户端调用的,所以如果是非静态的话,那就变成一个矛盾体了,因为非静态的方法必须要拥有实例才可以调用。
4.判断只有持有的静态实例为null时才调用构造方法创造一个实例,否则就直接返回。
第二种:线程安全的单例构造方法
public class SynchronizedSingleton {
private static volatile SynchronizedSingleton synchronizedSingleton;
private SynchronizedSingleton(){};
public static SynchronizedSingleton getInstance(){
if (synchronizedSingleton == null) {
synchronized (SynchronizedSingleton.class) {
if (synchronizedSingleton == null) {
synchronizedSingleton = new SynchronizedSingleton();
}
}
}
return synchronizedSingleton;
}
}
这种方法用的是同步代码块,其实也可以用用同步方法实现线程安全,但是同步方法会消耗很多无谓的线程的等待时间,同步代码块只需要在当前实例为null,也就是实例还未创建时才进行同步,否则会直接返回,可以节省很多无谓的线程等待时间。
volatile 这个关键字的作用
1保证线程修改的可见性
Java语言编写的程序,有时为了提高运行效率,编译器会自动对其优化,把经常访问的变量缓存起来,程序在读取这个变量时有可能直接从缓存(例如寄存器)中读取,而不会去内存中读取。当多线程编程时,变量的值可能因为别的线程改变了,而该缓存的值不会相应的改变,从而造成读取的值与实变量的值不一致。
volatile 被设计用来修饰不同线程访问和修改的变量。被volatile 类型定义的变量,系统每次用到他时都直接从对应的内存中提取,而不会利用缓存。这样所有线程在任何时候拿到的变量的值都是相同的。
2禁止指令重排序
在Java内存模型(JMM)中,并不限制处理器的指令顺序,说白了就是在不影响结果的情况下,顺序可能会被打乱。
在执行synchronizedSingleton = new SynchronizedSingleton();这条命令语句时,JMM并不是一下就执行完毕的,即不是原子性,实质上这句命令分为三大部分:
- 为对象分配内存
- 执行构造方法语句,初始化实例对象
- 把synchronizedSingleton的引用指向分配的内存空间
在JMM中这三个步骤中的2和3不一定是顺序执行的,如果线程A执行的顺序为1、3、2,在第2步执行完毕的时候,恰好线程B执行第一次判空语句,则会直接返回synchronizedSingleton,那么此时获取到的synchronizedSingleton仅仅只是不为null,实质上没有初始化,这样的对象肯定是有问题的!
而volatile关键字的存在意义就是保证了执行命令不会被重排序,也就避免了这种异常情况的发生,所以这种获取单例的方法才是真正的安全可靠!
第三种 饿汉式加载
public class EagerSingleton {
//JVM加载这个类时就创建
private static EagerSingleton eagerSingleton = new EagerSingleton();
private EagerSingleton(){}
public static EagerSingleton getInstance(){
return eagerSingleton;
}
}
第四种 登记式(内部类延迟加载)
public class StaticSingleton {
private static class SingletonHolder{
private static StaticSingleton instance= new StaticSingleton();
}
private StaticSingleton(){}
public static StaticSingleton getInstance(){
return SingletonHolder.instance;
}
}
内部类这个方式和饿汉式是有区别的,内部类是可以延迟加载的,只有客户端在调用getInstance方法的时候,内部类的调用的才会被触发,也就是在需要实例的时候才会被触发,所以是懒汉式加载。