单例模式的实现
文章目录
单例模式:
定义:采取一定的方法保证整个软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(一般是静态方法)
单例模式的八种方式:
- 饿汉式(静态常量)
- 饿汉式(静态代码块)
- 懒汉式(线程不安全)
- 懒汉式(线程安全,同步方法)
- 懒汉式(“线程安全”,同步代码块)
- 双重锁
- 静态内部类
- 枚举法
饿汉式(静态常量)
实例代码
public class Singleton {
//使用final修饰是防止被继承
private static final Singleton instance= new Singleton();
//构造器私有化 外部不能new对象
private Singleton(){}
//提供一个静态的公开方法,当使用该方法时,才去创建instance
public static Singleton getInstance(){
return instance;
}
}
步骤:
- 构造器私有化 (私有化的目的是:防止new一个对象实例)
- 类的内部创建对象(在类加载的时候创建一个对象实例instance)
- 向外暴露一个静态的公共方法返回一个实例对象(getInstance()方法去获得一个实例)
- 代码实现
饿汉式(静态常量)的优缺点:
- 优点:写法简单,在类装载时就完成实例化。避免线程同步问题。
- 缺点:在类装载时就完成了实例化,没有达到Lazy Loading(懒加载)的效果。如果从来没有使用这个实例就会造成内存的浪费。
- 方法基于classloder机制避免多线程的同步问题,在这种方法instance在类加载的时候就完成了实例化,在单例模式中大多数是调用getInstance方法。但是导致类加载的原因有很多种,不确定有其他的方式(或其他的静态方法导致类加载),这样初始化就没有达到Lazy Loading懒加载的效果,如果还没使用该实例对象更造成了内存的浪费。
饿汉式(静态代码块)
实例代码
public class Singleton {
//构造器私有化 外部不能new对象
private Singleton(){}
//在本类内部创建对象实例
private static Singleton instance;
static{
//在静态代码块中实例对象
instance=new Singleton();
}
public static Singleton getInstance(){
return instance;
}
}
饿汉式(静态代码块)的优缺点:
与饿汉式静态常量的优缺点类似,也是在类加载的时候执行动态代码块中的代码。
懒汉式(线程不安全)
实例代码
public class Singleton2 {
//类初始化时,不初始化这个对象(延时加载,真正用的时候再创建)
private static Singleton2 instance;
//构造器私有化
private Singleton2(){}
//提供一个静态的公开方法,当使用该方法时,才去创建instance
public static Singleton2 getInstance(){
if(instance==null){
instance=new Singleton2();
}
return instance;
}
懒汉式(线程不安全)的优缺点:
- 起到了懒加载Lazy Loading的效果,但是只能在单线程下使用
- 如果在多线程下,一个线程进入了if(instance==null)判断语句块,还未来的及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。
懒汉式(线程安全,同步方法)
实例代码
public class Singleton2 {
//类初始化时,不初始化这个对象(延时加载,真正用的时候再创建)
private static Singleton2 instance;
//构造器私有化
private Singleton2(){}
//提供一个静态的公开方法,加入了同步处理的代码,解决线程安全问题
public static synchronized Singleton2 getInstance(){
if(instance==null){
instance=new Singleton2();
}
return instance;
}
懒汉式(线程安全,同步方法)的优缺点:
- 解决了线程不安全问题
- 效率太低,每个线程想要获取类的实例时候,执行getInstance()方法都需要进行同步。而其实这个方法只需要执行一次实例化代码就行,后面想要获得该类实例,直接return就行。
懒汉式(“线程安全”,同步代码块)
代码实例
public class Singleton2 {
//类初始化时,不初始化这个对象(延时加载,真正用的时候再创建)
private static Singleton2 instance;
//构造器私有化
private Singleton2(){}
//提供一个静态的公开方法,
public static Singleton2 getInstance(){
if(instance==null){
//本意时在判断后进行方法同步
synchronized(Singleton2.class){
singleton=new Singleton2();
}
}
return singleton;
}
懒汉式(“线程安全”,同步代码块)优缺点:
- 本意是对懒汉式线程安全的改进,改为同步产生实例化的代码块
- 但是这种同步并不能起到线程同步的作用。这种方法的缺点和第3类类似,多线程同时进入if判断时,会导致多个实例的创建导致无法达到线程安全。
双重锁(DoubleCheck)
实例代码
public class Singleton3 {
//类初始化时,不初始化这个对象(延时加载,真正用的时候再创建)
private volatile static Singleton3 Singleton3;
//构造器私有化
private Singleton3() {
}
//提供一个静态的公开方法,加入了双重检查的代码,解决线程安全问题,解决懒加载问题
public static Singleton3 newInstance() {
if (Singleton3 == null) {
synchronized (Singleton3.class) {
if (Singleton3 == null) {
Singleton3 = new Singleton3();
}
}
}
return Singleton3;
}
}
双重锁(DoubleCheck)优缺点:
-
双重锁概念多线程开发经常使用,两次if判断保证线程安全
-
这样实例化代码只执行一次,避免反复进行方法同步
-
线程安全,延迟加载,效率较高
-
但是也是有问题的Singleton3 = new Singleton3();这句操作并非一个原子操作。事实上在 JVM 中这句话大概做了下面 3 件事情。
1.给 instance 分配内存
2.调用 Singleton 的构造函数来初始化成员变量
3.将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
我们只需要将 instance 变量声明成 volatile 就可以了
5.有些人认为使用 volatile 的原因是可见性,也就是可以保证线程在本地不会存有 instance 的副本,每次都是去主内存中读取。但其实是不对的。使用 volatile 的主要原因是其另一个特性:禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完 1-2-3 之后或者 1-3-2 之后,不存在执行到 1-3 然后取到值的情况。从「先行发生原则」的角度理解的话,就是对于一个 volatile 变量的写操作都先行发生于后面对这个变量的读操作(这里的“后面”是时间上的先后顺序)。
但是特别注意在 Java 5 以前的版本使用了 volatile 的双检锁还是有问题的。其原因是 Java 5 以前的 JMM (Java 内存模型)是存在缺陷的,即时将变量声明成 volatile 也不能完全避免重排序,主要是 volatile 变量前后的代码仍然存在重排序问题。这个 volatile 屏蔽重排序的问题在 Java 5 中才得以修复,所以在这之后才可以放心使用 volatile。
静态内部类
代码实例
public class Singleton4 {
//构造器私有化
private Singleton4(){}
//写一个静态内部类,该类中有一个静态属性Singleton4
private static class SingletonClassInstance{
private static final Singleton4 INSTANCE=new Singleton4();
}
//提供一个静态方法,直接返回SingletonClassInstance.INSTANCE
public static Singleton4 getInstance(){
return SingletonClassInstance.INSTANCE;
}
}
静态内部类优缺点:
- 这种方式采用了类加载的机制来保证初始化
- 静态内部类方法在Singleton类被加载时并不会立即实例化,而是在需要实例化时,调用getInstance方法才会装载SingletonClassInstance内部类,从而实现Singleton的实例化
- 类的静态属性只会在第一次加载时初始化一次,所以在这里JVM帮助我们保证了线程的安全性,在类进行初始化时别的线程是无法进入的
- 避免了线程不安全,利用静态内部类特点实现延迟加载,效率高
枚举
实例代码
public enum Singleton4 {
//枚举元素本身就是单例
INSTANCE;
//添加自己需要的操作
public void singletonOperation(){
}
//测试
public static void main(String[] args) {
Singleton4 instance=Singleton4.INSTANCE;
Singleton4 instance2=Singleton4.INSTANCE;
System.out.println(instance==instance2);
}
}
枚举优缺点:
- 借助JDK1.5中的枚举实现单例模式,不仅可以避免多线程的问题,还可以防止反序列化重新创建对象。
- 我们可以通过EasySingleton.INSTANCE来访问实例,这比调用getInstance()方法简单多了。创建枚举默认就是线程安全的,所以不需要担心double checked locking,而且还能防止反序列化导致重新创建新的对象。