Java设计模式之单例模式

单例模式,是特别常见的一种设计模式,因此我们有必要对它的概念和几种常见的写法非常了解,而且这也是面试中常问的知识点。

所谓单例模式,就是所有的请求都用一个对象来处理,如我们常用的Spring默认就是单例的,而多例模式是每一次请求都创建一个新的对象来处理,如structs2中的action。

使用单例模式,可以确保一个类只有一个实例,并且易于外部访问,还可以节省系统资源。如果在系统中,希望某个类的对象只存在一个,就可以使用单例模式。

那怎么确保一个类只有一个实例呢?

我们知道,通常我们会通过new关键字来创建一个新的对象。这个时候类的构造函数是public公有的,你可以随意创建多个类的实例。所以,首先我们需要把构造函数改为private私有的,这样就不能随意new对象了,也就控制了多个实例的随意创建。

然后,定义一个私有的静态属性,来代表类的实例,它只能类内部访问,不允许外部直接访问。

最后,通过一个静态的公有方法,把这个私有静态属性返回出去,这就为系统创建了一个全局唯一的访问点。

以上,就是单例模式的三个要素。总结为:

私有构造方法

指向自己实例的私有静态变量

对外的静态公共访问方法

单例模式分为饿汉式和懒汉式。它们的主要区别就是,实例化对象的时机不同。饿汉式,是在类加载时就会实例化一个对象。懒汉式,则是在真正使用的时候才会实例化对象。

饿汉式单例代码实现:

public class Singleton { // 饿汉式单例,直接创建一个私有的静态实例 private static Singleton singleton = new Singleton(); //私有构造方法 private Singleton(){ } //提供一个对外的静态公有方法 public static Singleton getInstance(){ return singleton; } }

懒汉式单例代码实现

public class Singleton { // 懒汉式单例,类加载时先不创建实例 private static Singleton singleton = null; //私有构造方法 private Singleton(){ } //真正使用时才创建类的实例 public static Singleton getInstance(){ if(singleton == null){ singleton = new Singleton(); } return singleton; } }

稍有经验的程序员就发现了,以上懒汉式单例的实现方式,在单线程下是没有问题的。但是,如果在多线程中使用,就会发现它们返回的实例有可能不是同一个。我们可以通过代码来验证一下。创建十个线程,分别启动,线程内去获得类的实例,把实例的 hashcode 打印出来,只要相同则认为是同一个实例;若不同,则说明创建了多个实例。

public class TestSingleton { public static void main(String[] args) { for (int i = 0; i < 10 ; i++) { new MyThread().start(); } } } class MyThread extends Thread { @Override public void run() { Singleton singleton = Singleton.getInstance(); System.out.println(singleton.hashCode()); } } /** 运行多次,就会发现,hashcode会出现不同值 668770925 668770925 649030577 668770925 668770925 668770925 668770925 668770925 668770925 668770925 */

所以,以上懒汉式的实现方式是线程不安全的。那饿汉式呢?你可以手动测试一下,会发现不管运行多少次,返回的hashcode都是相同的。因此,认为饿汉式单例是线程安全的。

那为什么饿汉式就是线程安全的呢?这是因为,饿汉式单例在类加载时,就创建了类的实例,也就是说在线程去访问单例对象之前就已经创建好实例了。而一个类在整个生命周期中只会被加载一次。因此,也就可以保证实例只有一个。所以说,饿汉式单例天生就是线程安全的。(可以了解一下类加载机制)

既然懒汉式单例不是线程安全的,那么我们就需要去改造一下,让它在多线程环境下也能正常工作。以下介绍几种常见的写法:

1) 使用synchronized方法

实现非常简单,只需要在方法上加一个synchronized关键字即可

public class Singleton { private static Singleton singleton = null; private Singleton(){ } //使用synchronized修饰方法,即可保证线程安全 public static synchronized Singleton getInstance(){ if(singleton == null){ singleton = new Singleton(); } return singleton; } }

这种方式,虽然可以保证线程安全,但是同步方法的作用域太大,锁的粒度比较粗,因此,执行效率就比较低。

2) synchronized 同步块

既然,同步整个方法的作用域大,那我缩小范围,在方法里边,只同步创建实例的那一小部分代码块不就可以了吗(因为方法较简单,所以锁代码块和锁方法没什么明显区别)。

public class Singleton { private static Singleton singleton = null; private Singleton(){ } public static Singleton getInstance(){ //synchronized只修饰方法内部的部分代码块 synchronized (Singleton.class){ if(singleton == null){ singleton = new Singleton(); } } return singleton; } }

这种方法,本质上和第一种没什么区别,因此,效率提升不大,可以忽略不计。

3) 双重检测(double check)

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wssxzp.html