一、概述
单例模式,保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例模式,也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。
常规情况:
package DP03.demo31; public class Printer { public Printer(String text) { this.text = text; } public String text; public void show() { System.out.println(this.text); } public static void main(String[] args) { //打印机实例一 Printer p1 = new Printer("gd"); //打印机实例二 Printer p2 = new Printer("nf"); p1.show(); p2.show(); } }
结果:
默认new出的对象是多个不同的实例,引用与成员是相对独立的,而我们的目的是想这里的p1与p2共用同一个实例。
二、实现单例模式
2.1、实例化控制
Java中接口与抽象类不能直接实例化的:
package DP03.demo31; public class Test { public static void main(String[] args) { IFlyable flyable=new IFlyable(); //错误 Bird bird=new Bird(); //错误 } } interface IFlyable { void fly(); } abstract class Bird { abstract void eat(); }
当然使用匿名内部类是可以的,但本质还是实现了接口后实例化的:
package DP03.demo31; public class Test { public static void main(String[] args) { //IFlyable flyable=new IFlyable(); //错误 //Bird bird=new Bird(); //错误 String name="鸟"; //匿名内部类 IFlyable flyable= new IFlyable() { @Override public void fly() { System.out.println(name+"飞。。。"); } }; flyable.fly(); //匿名内部类 Bird bird= new Bird() { @Override void eat() { System.out.println(name+"吃。。。"); } }; bird.eat(); } } interface IFlyable { void fly(); } abstract class Bird { abstract void eat(); }
运行结果:
对象的实例化总是会调用某个构造函数,如果想控制对象的实例化问题,只能在构造函数上做文章
2.2、单例模式
实现单例模式的思路是:定义私有对象的引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名称);当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;同时我们还将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例
示例:
package DP03.demo31; public class PrinterPro { private PrinterPro() { } //定义唯一的实例 private static PrinterPro printer=null; //对外暴露的访问接口 public static PrinterPro GetPrinter(String text){ //如果printer为空,第一次访问 if(printer==null){ printer=new PrinterPro(); //实例化 printer.text=text; } return printer; //返回实例 } public String text; public void show() { System.out.println(this.text); } }
注意构造方法是私有的(限制new),GetPrinter是一个全局访问点,用于返回创建好的实例。
测试代码:
package DP03.demo31; public class PrinterProTest { public static void main(String[] args) { //打印机实例一 PrinterPro p1 = PrinterPro.GetPrinter("gd"); //打印机实例二 PrinterPro p2 = PrinterPro.GetPrinter("nf"); p1.show(); p2.show(); System.out.println(p1==p2); } }
运行结果:
2.3、懒汉模式
在单例模式的实现中主要有2种方式,一种是在调用方法的时候进行实例化的工作,这种方式俗称懒汉模式,而另外一种方式是在类的加载的时候就创建一个实例,这种方式俗称饿汉模式。
在懒汉模式实现的单例模式中,创建对象实例的代码中有一个判断是否为null的情况,如果在多线程环境下,如果线程A进入这个方法体的时候,发现对象为null,那么会创建这个类的一个实例,而另外一个线程B,也紧跟着线程A进入了这个方法体中,线程B检查对象是否为空的时候,也得出了对象为空,那么线程B也会创建一个类型的实例,这样就会导致这个类在懒汉模式的实现中会出现创建多个实例的问题。
package DP03.demo032; public class Printer { private Printer() { } private static Printer obj = null; public static Printer getPrinter() { if (obj == null) obj = new Printer(); return obj; } }
在多线程环境下的问题:
这样就有可能产生多个实例,为了防止这种问题可以使用:同步代码块或饿汉模式
2.4、饿汉模式
饿汉模式通过在类级别定义一个全局的静态变量,并且立即实例化本类的一个对象并赋值给这个变量,而在返回本类的静态方法中只需要返回这个全局变量就可以了。无需判断变量是否为空
package DP03.demo033; public class Printer { private Printer() { } private static Printer obj = new Printer(); public static Printer getPrinter() { return obj; } }
饿汉式和懒汉式区别
1、线程安全:
饿汉式是线程安全的,可以直接用于多线程而不会出现问题,懒汉式就不行,它是线程不安全的,如果用于多线程可能会被实例化多次,失去单例的作用。
如果要把懒汉式用于多线程,有两种方式保证安全性,一种是在getInstance方法上加同步,另一种是在使用该单例方法前后加双锁。
2、资源加载
饿汉式在类创建的同时就实例化一个静态对象出来,不管之后会不会使用这个单例,会占据一定的内存,相应的在调用时速度也会更快,
而懒汉式顾名思义,会延迟加载,在第一次使用该单例的时候才会实例化对象出来,第一次掉用时要初始化,如果要做的工作比较多,性能上会有些延迟,之后就和饿汉式一样了。
如果想不预先实例化,又想线程安全,可以使用同步锁。
2.5、对象的数量特定的“单例”模式
单例模式,顾名思义是创建类型的唯一一个实例,在某些情况下,我们会有创建特定数量实例的需求,比如人有2个手,那么实例化手这个类的时候,我们希望只创建出2个手,而狗有四条腿,我们实例化腿这个类型的时候,希望创建这个类型的四个实例给使用者。
package DP03.demo034; import java.util.ArrayList; public class Printer { //实例个数 public static final int MAX = 5; private Printer() { } private static ArrayList<Printer> printers = new ArrayList<Printer>(); static { for (int i = 0; i < MAX; i++) { printers.add(new Printer()); } } public ArrayList<Printer> getPrinters() { return printers; } }
如果想返回单个实例:
package DP03.demo035; import java.util.ArrayList; public class Printer { //实例个数 public static final int MAX = 3; //当前下标 public static int index =-1; public static int getIndex() { index++; if (index >= MAX) index = 0; return index; } private Printer() { } private static ArrayList<Printer> printers = new ArrayList<Printer>(); static { for (int i = 0; i < MAX; i++) { printers.add(new Printer()); } } public static Printer getPrinter() { return printers.get(getIndex()); } }
测试:
package DP03.demo035; public class PrinterTest { public static void main(String[] args) { for (int i=0;i<=80;i++){ Printer p=Printer.getPrinter(); System.out.println(p); } } }
运行结果:
三、总结
适用范围:
非常耗资源的对象创建时(比如读取大文件)
资源的创建比较昂贵(比如数据库的连接)
逻辑上只应该有一个(比如winform中某些窗口只应打开一次)
四、示例
示例:https://coding.net/u/zhangguo5/p/DP01/git
五、视频
视频:https://www.bilibili.com/video/av15867320/
六、作业与资料
作业:
a)、请同时使用“懒汉模式”与“饿汉模式”完成一个打印机类(Print),要求达到单例效果,分开完成,两个类,两个测试。
b)、请将上一题变成“可指定实例数量”的单例模式,比如5个,访问时实现平均分配对象,与上一题分开。
c)、加同步锁实现线程完全的单例模式(不预先实例化,又想线程安全,修改懒汉模式)