java中单例模式是一种常见的设计模式,这里主要介绍两种单例模式:
懒汉模式、饿汉模式
1.什么是单例模式?
保证整个系统中一个类只有一个对象的实例,实现这种功能方式就叫单例模式(通俗的讲:一个类中只有一个实例,并提供一个全局访问点)
2.为什么要使用单例模式?
1.单例模式节省公共资源
如:大家都要喝水,但是没必要每人家里都打一口井是吧,通常的做法是整个村里打一个井就够了,大家都从这个井里面打水喝。
对应到我们计算机里,象日志管理、打印机、数据库连接池、线程池等
2.单例模式方便控制
就像日志管理,如果多个人同时来写日志,你一笔我一笔那整个日志文件都乱七八糟,如果想要控制日志的正确性,那么必须要对关键的代码进行上锁,只能一个一个按照顺序来写,而单例模式只有一个人来向日志里写入信息方便控制,避免了这种多人干扰的问题出现。
3.
单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。
3.单例模式具有以下特点:
1.单例类只能有一个实例。
2.单例类必须自己创建自己的唯一实例。
3.单例类必须给所有其他对象提供这一单例。
实现单例模式的思路
1.构造私有
如果想要保证一个类不被多次实例化,那么我们就要阻止对象别new出来。那么方法就是:把类的所有构造方法变成私有化
2.已静态方法返回实例
因为外界无法通过new来获得对象,所以我们要通过类的方法来让外界获取对象实例。
3.确保对象实例只有一个
只对类进行一次实例化,以后都直接获取第一次实例化的对象
/**
* 单例模式案例
*/
public class Singleton {
// 确保对象实例只有一个
private static Singleton singleton = new Singleton();
//构造方法私有化
private Singleton(){
}
//已静态方法返回实例
public static Singleton getInstance(){
return singleton;
}
}
注:这里类的实例在类初始化的时候就已经生成,不再进行第二次实例化了。而外界只能通过 Singleton.getInstance()方法来获取Singleton对象,所以这样就保证一个类只有一个实例了。
饿汉模式:
饿汉模式的意思是:我先把对象(面包)创建好,等我要用(吃)的时候,直接拿出来就行了
1.先创建好单例
//饿汉式单例:在类初始化时,就已经实例化
public class SingletonDemo3 {
//私有化构造方法
private SingletonDemo3(){}
//先将对象创建好
private static final SingletonDemo3 singleton=new SingletonDemo3();
//其他人来拿的时候直接返回自己创建好的对象
public static SingletonDemo3 getInstance(){
return singleton;
}
2.对单例的测试
public class SingletonTest2 {
public static void main(String[] args) {
SingletonDemo3 singleton = SingletonDemo3.getInstance();
SingletonDemo3 singleton2 = SingletonDemo3.getInstance();
System.out.println(singleton.toString());
System.out.println(singleton2.toString());
}
}
注:因为这本身就是static修饰的方法,所以是在类加载的时候被创建,后期不会再改变,所以线程是安全的
3.结果:
Singleton.SingletonDemo3@1b6d3586
Singleton.SingletonDemo3@1b6d3586
上面的案例就是饿汉模式,这种饿汉模式是最简单的最省心的,不足的地方容易会造成资源浪费(我事先把面包都做好了,但是你并不一定吃,这样容易造成资源的浪费)。
懒汉模式:
因为饿汉模式可能会造成资源浪费的问题,所以就有了懒汉模式
懒汉模式的意思是:我先不创建类的对象实例,等你需要的时候我再创建
1.先创建单例
/**
* 单例模式案例
*/
public class Singleton {
private static Singleton singleton =null;
//私有化构造方法
private Singleton(){
}
//第一次调用的时候再进行实例化
public Singleton getSingleton(){
if (singleton==null){
singleton = new Singleton();
System.out.println("创建一次");
}
return singleton;
}
public void Show(){
System.out.println("我是show");
}
}
2.测试单例
public class SingletonTest2 {
public static void main(String[] args) {
Singleton singleton1 = Singleton.getSingleton();
Singleton singleton2 = Singleton.getSingleton();
System.out.println(singleton1==singleton2);
singleton1.Show();
singleton2.Show();
}
}
3.结果:
创建一次
true
我是show
我是show
所以我们可以看出就算多创建几个对象,在底部也只有一个Singleton对象实例,而且创建出来的字符串也是一样的
懒汉模式在并发情况下可能引起的问题
懒汉模式解决了饿汉模式可能引起的资源浪费问题,因为这种模式只有在用户要使用的时候才会实例化对象。但是这种模式在并发情况下会出现创建多个对象的情况。
因为可能出现外界多人同时访问Singleton.getInstance()方法,这里可能会出现因为并发问题导致类被实例化多次,所以懒汉模式需要加上锁synchronized (Singleton.class) 来控制类只允许被实例化一次。
如果不加锁并发的情况下会出现这种情况
加锁后就不会出现多个线程同时执行相同代码的情况,因为线程是按队列的形式执行的,只有当前一个线程执行完之后才能进入代码块。
懒汉模式加锁引起的性能问题
在上面的案例中,我们通过锁的方式保证了单例模式的安全性,因为获取对象的方法加锁,多人同时访问只能排队等上一个人执行完才能继续执行,但加锁的方式会严重影响性能。
解决方案一:双重检查加锁(DCL)
public static Singleton getSingleton(){
if (singleton==null) { //先检验对象是否被创建
synchronized (Singleton.class) { //只有对象未被创建的时候才上锁
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
双检测锁定的方式 是只有当对象未创建的时候才对请求加锁,对象创建以后都不会上锁,这样有效的提升了程序的效率,也可以保证只会创建一个对象的实例。
DCL是完美的解决了单例模式中性能和资源浪费的问题,但是DCL在并发情下也会存在一个问题,因为Jvm指令是乱序的;
情况如下:
线程1调用getInstance 获取对象实例,因为对象还是空未进行初始化,此时线程1会执行new Singleton()进行对象实例化,而当线程1的进行new Singleton()的时候JVM会生成三个指令。
指令1:分配对象内存。
指令2:调用构造器,初始化对象属性。
指令3:构建对象引用指向内存。
因为编译器会自作聪明的对指令进行优化, 指令优化后顺序会变成这样:
1、执行指令1:分配对象内存,
2、执行指令3:构建对象引用指向内存。
3、然后正好这个时候CPU 切到了线程2工作,而线程2此时也调用getInstance获取对象,那么线程2将执行下面这个代码 if (singleton == null),此时线程2发现对象不为空(因为线程1已经创建对象引用并分配对象内存了),那么线程2会得到一个没有初始化属性的对象(因为线程1还没有执行指令2)。
所以在这种情况下,双检测锁定的方式会出现DCL失效的问题。
解决方案二:用内部类实现懒汉模式
public class Singleton {
private static Singleton singleton =null;
//私有化构造方法
private Singleton(){
}
public static Singleton getInstance(){
return SingletonHoler.singleton;
}
//定义静态内部类
private static class SingletonHoler{
//当内部类第一次被访问时,才创建对象
private static Singleton singleton =new Singleton();
}
}
静态内部类原理:
当外部内被访问时,并不会加载内部类,所以只要不访问SingletonHoler 这个内部类, private static Singleton singleton = new Singleton() 不会实例化,这就相当于实现懒加载的效果,只有当SingletonHoler.singleton 被调用时访问内部类的属性,此时才会将对象进行实例化,这样既解决了恶汉模式下可能造成资源浪费的问题,也避免了了懒汉模式下的并发问题。