【设计模式】单例模式 - 指南
系列文章目录

类也需要计划生育
我们写一个窗体程序时,当中有一个是‘工具箱’窗体,问题就是,我们希望工具箱要么不出现,出现也只出现一个,而实际上却是我每次单击菜单,实例化‘工具箱’,就会出现一个,多次点击就会出现多个?这里我们应该怎么办呢?
我们的第一版代码是这样的,首先我们建立一个Java的swing窗体应用程序,默认窗体为JFame,左上角有一个打开工具箱按钮,我们希望单击此按钮后,可以另建一个窗体,也就是‘工具箱’窗体,里面可以有一些相关工具按钮。
public class Test
{
public static void main(String[] args) {
new SingletonWindow();
}
}
class SingletonWindow
{
public SingletonWindow(){
JFrame frame = new JFrame("单例模式");
frame.setSize(1024,768);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JPanel panel = new JPanel();
frame.add(panel);
panel.setLayout(null);
JButton button = new JButton("打开工具箱");
//容器中有一个JButton按钮,叫打开工具箱
button.setBounds(10,10,120,25);
button.addActionListener(new ActionListener() {
//按钮单击会触发打开一个叫工具箱的JFrame窗体
@Override
public void actionPerformed(ActionEvent e) {
JFrame toolkit = new JFrame("工具箱");
toolkit.setSize(150,300);
toolkit.setLocation(100,100);
toolkit.setResizable(false);
toolkit.setAlwaysOnTop(true);
toolkit.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
toolkit.setVisible(true);
}
});
panel.add(button);
frame.setVisible(true);
}
}
上述代码,我们每单击一次‘打开工具箱’按钮,就会产生一个新的‘工具箱’窗体,但实际上,我们只希望它出现第一次,不会出现第二次。除非单击关闭之后,才会再次出现。
判断对象是否为null
这里我们需要做的是判断JFrame有没有实例化就好了。在上述代码中,我们是在单击了按钮时,才去JFrame tookit = new JFrame(“工具箱”);当然是新实例化了。但其实问题也就出现在这里,为什么要单击按钮时才声明JFrame对象呢?,我们完全可以把声明的工作放到类的全局变量中完成。这样就可以去判断这个变量是否被实例化过了。
public class Test
{
public static void main(String[] args) {
new SingletonWindow();
}
}
class SingletonWindow
{
public SingletonWindow(){
JFrame frame = new JFrame("单例模式");
frame.setSize(1024,768);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JPanel panel = new JPanel();
frame.add(panel);
panel.setLayout(null);
JButton button = new JButton("打开工具箱");
//容器中有一个JButton按钮,叫打开工具箱
button.setBounds(10,10,120,25);
button.addActionListener(new ActionListener() {
//按钮单击会触发打开一个叫工具箱的JFrame窗体
JFrame toolkit;
@Override
public void actionPerformed(ActionEvent e) {
if(toolkit == null || !toolkit.isVisible()){
//判断是否实例化,如果没有或隐藏,那么就重新实例化
JFrame toolkit = new JFrame("工具箱");
toolkit.setSize(150,300);
toolkit.setLocation(100,100);
toolkit.setResizable(false);
toolkit.setAlwaysOnTop(true);
toolkit.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
toolkit.setVisible(true);
}
}
});
panel.add(button);
frame.setVisible(true);
}
}
基本需求上述代码可以实现,现在我们需求,不但要在菜单里启动‘工具箱’,还需要‘工具栏’上有一个按钮来启动‘工具箱’,如何做?也就是要有两个按钮都要能打开这个工具箱。我们可以提炼一个方法来让它们调用。
//工具箱事件类
class ToolkitListener
implements ActionListener {
JFrame toolkit;
@Override
public void actionPerformed(ActionEvent e) {
if (toolkit == null || !toolkit.isVisible()) {
//判断是否实例化,如果没有或隐藏,那么就重新实例化
JFrame toolkit = new JFrame("工具箱");
toolkit.setSize(150, 300);
toolkit.setLocation(100, 100);
toolkit.setResizable(false);
toolkit.setAlwaysOnTop(true);
toolkit.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
toolkit.setVisible(true);
}
}
}
//窗体类
class SingletonWindow
{
public SingletonWindow() {
JFrame frame = new JFrame("单例模式");
frame.setSize(1024, 768);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JPanel panel = new JPanel();
frame.add(panel);
panel.setLayout(null);
JButton button = new JButton("打开工具箱");
//容器中有一个JButton按钮,叫打开工具箱
button.setBounds(10, 10, 120, 25);
button.addActionListener(new ToolkitListener());
panel.add(button);
JButton button2 = new JButton("打开工具箱2");
//容器中有一个JButton按钮,叫打开工具箱
button.setBounds(130, 10, 120, 25);
button.addActionListener(new ToolkitListener());
panel.add(button2);
frame.setVisible(true);
}
}
这样可以分别单击打开工具箱和打开工具箱2,此时两个按钮分别打开了一个工具箱窗体,这依然不符合我们只打开一个‘工具箱’的需求。
目前的代码中我们将工具箱‘JFrame’是否实例化都由外部的代码决定,这好像很不合逻辑。
class ToolkitListener
implements ActionListener {
JFrame toolkit;
@Override
public void actionPerformed(ActionEvent e) {
if (toolkit == null || !toolkit.isVisible()) {
//判断是否实例化,如果没有或隐藏,那么就重新实例化
JFrame toolkit = new JFrame("工具箱");
toolkit.setSize(150, 300);
toolkit.setLocation(100, 100);
toolkit.setResizable(false);
toolkit.setAlwaysOnTop(true);
toolkit.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
toolkit.setVisible(true);
}
}
}
在主窗体里,应该只是通知启动‘工具箱’,至于工具箱窗体是否实例化过,应该有工具箱自己的类来判断。实例化的过程就是new的过程,问题是如何让外部代码不使用new呢?这里我们只能对构造方法做改动,不可能阻止他人不使用new的,所以我们可以直接就把这个类的构造方法改成私有,所有类的构造方法,不编码则系统默认生成空的构造方法,若有显示的定义的构造方法,默认的构造方法就会失效。 所以我们需要将‘工具箱’类的构造方法写成是private的,那么外部程序就不能用new 来实例化他了。
私有的方法外界不能访问,但是这样来看,这个类一个实例化都不能了,这与我们的让这个类实例化一次又矛盾了。这里这样理解是有问题的,经过对构造方法的private修饰,我们只能说对于外部代码,不能用new来实例化它,但是我们完全可以再写一个public 方法叫做getInstance(),这个方法的目的就是返回一个类实例,而此方法中,去做是否有实例化的判断。如果没有实例化,由调用private的构造方法new出这个实例,之后它就可以调用,因为在同一个类中,所以private方法是可以被调用的。
//工具箱类
class Toolkit
extends JFrame {
private static Toolkit toolkit;
private Toolkit(String title){
super(title);
}
public static Toolkit getInstance(){
//判断是否实例化,如果没有或隐藏,可以实例化
if (toolkit == null || !toolkit.isVisible()) {
JFrame toolkit = new JFrame("工具箱");
toolkit.setSize(150, 300);
toolkit.setLocation(100, 100);
toolkit.setResizable(false);
toolkit.setAlwaysOnTop(true);
toolkit.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
toolkit.setVisible(true);
}
return toolkit;
//如果已经存在,就直接返回存在的实例,并不在new新的实例
}
}
有了上面的代码,我们在写工具箱事件类时,就可以改造如下
//工具箱事件类
class ToolkitListener
implements ActionListener{
@Override
public void actionPerformed(ActionEvent e) {
//Toolkit toolkit = new Toolkit("工具箱"); 传统的new来获得实例会报错
Toolkit.getInstance();
//实例化唯一的一个对象 -- 单例对象
}
}
上面的代码可以达到一个效果,只有在实例不存在时,才会去new新的实例,而但存在时,可以直接返回存在的同一个实例。这就是设计模式 – 单例模式
单例模式
> 单例模式: 保证一个类仅有一个实例,并提供一个访问它的全局访问点。
通常我们可以让一个全局变量使得一个对象被访问,但它不能防止你实例化多个对象。一个最好的方法就是,让类自身负责保存它的唯一实例。这个类可以保证没有其他实例可以被创建,并且他可以提供一个访问该实例的方法 。
Singleton类,定义一个GetInstance操作,允许客户访问它的唯一实例。GetIntance是一个静态方法,主要负责创建自己的唯一实例
//单例模式类
class Singleton
{
private static Singleton instance;
private Singleton{
}
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
客户端代码:
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
if(s1 == s2){
System.out.println("两个对象是相同的实例");
}
}
单例模式不光可以保证只存在唯一的实例,另外单例模式因为Singleton类封装它的唯一实例,这样它可以严格的控制顾客怎样访问他以及何时访问它。简单的说就是对唯一实例的受控访问。
多线程时的单例
在多线程的程序中,多个线程同时访问Singleton类,调用getInstance(),调用getInstance()方法,会有可能造成创建多个实例。
我们可以给进程一把锁来处理,这里需要解释一下synchronized语句的含义。synchronized是Java中的关键字,是一种同步锁。意思就是当一个线程还没有退出之前,先锁住这段代码不被其他其他线程代码调用执行,以保证同一时间只有一个线程在执行此段代码。
class Singleton
{
private static Singleton instance;
private Singleton(){
}
//通过增加synchronized关键字到getInstance方法中,可以让每个线程在进入此方法之前,都要等到别的线程离开此方法
public static synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
这段代码使得对象实例由最先进入的那个线程来创建,以后的线程在进入时不会再去创建对象实例了,由于有了synchronized,就保证了多线程环境下的同时访问也不会造成多个实例的生成。这里为什么不直接锁实例,而是使用synchronized锁是因为加锁的时候,我们不清楚instance是否已经被创建,也就无法加锁。但是这样的话,每次调用getInstance()都需要锁,性能降低,所以这个代码还是有可以优化的地方的。
双重锁定
class Singleton
{
//volatile关键词是当synchronized变量初始化成Single时,多个线程能够正确地处理synchronized变量
private volatile static Singleton instance;
private Singleton(){
}
public static Singleton getInstance(){
if(instance == null){
synchronized (Singleton.class)
{
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
这版的代码,我们不用让线程每次都加锁,而只是在实例未被创建的时候再加锁处理。同时也能保证多线程的安全,这种方法叫做Double-Check Locking(双重否定)。
那么这里为什么在外面已经判断了instance的实例是否存在,为什么在synchronized里面还需要做一次instance实例是否存在的判断呢?对于instance存在的情况就直接返回,这没问题,当instance为null并且同时有两个线程调用getInstance()方法时,他们将都可以通过第一重instance == null的判断,然后由于锁机制,这两个线程则只有一个进入,另一个在排队等待,必须要其中的一个进入并出来后,另一个才能进去,而此时没有了instance == null的判断,就会创建多个实例,没有达到单例的目的
静态初始化
上述代码基本对实际应用中的大部分情况都能应付自如,不过为了确保实例唯一,还是会带来很大的性能代价,对于那些性能要求特别高的程序来说,传统单例代码实现还不是最好的办法。
class Singleton
{
private static Singleton instance = new Singleton();
//构造方法private化
private Singleton(){
}
//得到Singleton的实例
public static Singleton getInstance(){
return instance;
}
}
这样的实现与前面的示例相似,解决了单例模式的全局访问和实例化控制,公共静态属性为访问实例提供了一个全局访问点。不同之处在于它依赖公共语言运行库来初始化变量。由于构造方法是私有的,因此不能在类本身以外实例化Singleton类;因此,变量引用的是可以在系统中存在的唯一实例。由于这种静态初始化的方式是在自己被加载时就将自己实例化,所以被形象的称之为饿汉式单例类,原先的单例模式的处理方式是要在第一次被引用时,才会将自己实例化,所以就被称为懒汉式单例类。
总结
以上就是本文全部内容,本文主要向大家介绍了设计模式中的单例模式,通过对窗口的单例创建为例,引出单例模式的模板代码,之后介绍了多线程下的单例模式。介绍了双重否定和静态初始化两种实现单例模式的方法。 感谢各位能够看到最后,如有问题,欢迎各位大佬在评论区指正,希望大家可以有所收获!创作不易,希望大家多多支持!

浙公网安备 33010602011771号