java 单例模式

java 单例模式,第1张

JAVA单例模式的几种实现方法
1饿汉式单例类
package patternsingleton;
//饿汉式单例类在类初始化时,已经自行实例
public class Singleton1 {
//私有的默认构造子
private Singleton1() {}
//已经自行实例化
private static final Singleton1 single = new Singleton1();
//静态工厂方法
public static Singleton1 getInstance() {
return single;
}
}
2懒汉式单例类
package patternsingleton;
//懒汉式单例类在第一次调用的时候实例化
public class Singleton2 {
//私有的默认构造子
private Singleton2() {}

//注意,这里没有final
private static Singleton2 single;

//只实例化一次
static{
single = new Singleton2();
}

//静态工厂方法
public synchronized static Singleton2 getInstance() {
if (single == null) {
single = new Singleton2();
}
return single;
}
}
在上面给出懒汉式单例类实现里对静态工厂方法使用了同步化,以处理多线程环境。有些设计师在这里建议使用所谓的"双重检查成例"必须指出的是,"双重检查成例"不可以在Java 语言中使用。不十分熟悉的读者,可以看看后面给出的小节。 同样,由于构造子是私有的,因此,此类不能被继承。饿汉式单例类在自己被加载时就将自己实例化。即便加载器是静态的,在饿汉式单例类被加载时仍会将自己实例化。单从资源利用效率角度来讲,这个比懒汉式单例类稍差些。从速度和反应时间角度来讲,则比懒汉式单例类稍好些。然而,懒汉式单例类在实例化时,必须处
理好在多个线程同时首次引用此类时的访问限制问题,特别是当单例类作为资源控制器,在实例化时必然涉及资源初始化,而资源初始化很有可能耗费时间。这意味着出现多线程同时首次引用此类的机率变得较大。
饿汉式单例类可以在Java 语言内实现, 但不易在C++ 内实现,因为静态初始化在C++ 里没有固定的顺序,因而静态的m_instance 变量的初始化与类的加载顺序没有保证,可能会出问题。这就是为什么GoF 在提出单例类的概念时,举的例子是懒汉式的。他们的书影响之大,以致Java 语言中单例类的例子也大多是懒汉式的。实际上,本书认为饿汉式单例类更符合Java 语言本身的特点。
3登记式单例类
package patternsingleton;
import javautilHashMap;
import javautilMap;
//登记式单例类
//类似Spring里面的方法,将类名注册,下次从里面直接获取。
public class Singleton3 {
private static Map map = new HashMap();
static{
Singleton3 single = new Singleton3();
mapput(singlegetClass()getName(), single);
}

//保护的默认构造子
protected Singleton3(){}

//静态工厂方法,返还此类惟一的实例
public static Singleton3 getInstance(String name) {
if(name == null) {
name = Singleton3classgetName();
Systemoutprintln("name == null"+"--->name="+name);
}
if(mapget(name) == null) {
try {
mapput(name, (Singleton3) ClassforName(name)newInstance());
} catch (InstantiationException e) {
eprintStackTrace();
} catch (IllegalAccessException e) {
eprintStackTrace();
} catch (ClassNotFoundException e) {
eprintStackTrace();
}
}
return mapget(name);
}

//一个示意性的商业方法
public String about() {
return "Hello, I am RegSingleton";
}
public static void main(String[] args) {
Singleton3 single3 = Singleton3getInstance(null);
Systemoutprintln(single3about());
}
}

1 定义

单例模式指的是一个类,在全局范围内(整个系统中)有且只能有一个实例存在。即该类本身负责提供一种访问其唯一对象的方式,不对外提供公共的构造函数(禁用默认公共构造函数),对于该类的实例化由它自己在类的内部进行维护!

2 优缺点

- 优点

1 最大程度的减少了对象的创建和销毁的次数,从而降低的垃圾回收的次数

2 节约了系统资源,尤其是内存资源

- 缺点

1 不能继承,不能被外部实例化

2 类干预了外部类的使用(外部实用类不能随意实例化),而不再仅仅专注于内部的逻辑(与单一职责模式有矛盾)

3 使用场景

- 有频繁的实例化后又销毁的情况,适合考虑使用单例模式,如记录日志的log对象

- 创建对象需要消耗过多的系统资源,但又经常用到的资源,如数据库连接

4 框架中的应用

5 实现方式

单例模式有多种实现方式,要考虑到多线程下的安全性,其每种实现方式如下所示:

以上方式,如果存在多个线程同时访问getInstance()时,由于没有锁机制,会导致实例化出现两个实例的情况,因此,在多线程环境下时不安全的。

如上代码所示,在getInstance()方法上添加了同步锁。但是该方法虽然解决了线程安全的问题,但却也带来了另外的一个问题,就是每次获取对象时,都要先获取锁,并发性能很差,还需要继续优化!

该方法将方法上的锁去掉了,避免了每次调用该方法都要获取锁的 *** 作,从而提升了并发性能,同时在方法内部使用锁,进而解决了并发的问题,从而解决了上面并发安全+性能低效的问题,是个不错的实现单例的方式。

该方式虽然简单也安全,但是会造成再不需要实例时,产生垃圾对象,造成资源狼粪,因此,一般不使用。

这种方式可以达到跟 双重校验锁 一样的效果,但只适用于静态域的情况,双重校验锁可在实例域需要延迟初始化时使用

这是实现单例模式的最佳方法,更加简洁,自动支持序列化,防止多次实例化,非常高效! (强烈推荐使用)

6引用

/
  懒汉模式
  /
public class Singleton {

private volatile static Singleton uniqueInstance;

private Singleton(){}

public static Singleton getInstance(){
if(uniqueInstance==null){  //首先判断是否已创建,如果未创建,才进入同步代码块
                       //这样可以减少同步代码块带来的内存开销
synchronized(Singletonclass){ //synchronized将该类锁住,使得其他线程无法访问
if(uniqueInstance==null){  //再次检查是否创建过实例
uniqueInstance=new Singleton();
}
}
}

return uniqueInstance;
}
}/
  饿汉模式
  /
public class Singleton {

private  static Singleton uniqueInstance=new Singleton() ;

private Singleton(){}

public static Singleton getInstance(){
return uniqueInstance;
}
}

当被问到要实现一个单例模式时,很多人的第一反应是写出如下的代码,包括教科书上也是这样教我们的。
1
2
3
4
5
6
7
8
9
10
11
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance () {
if (instance == null ) {
instance = new Singleton();
}
return instance;
}
}
这段代码简单明了,而且使用了懒加载模式,但是却存在致命的问题。当有多个线程并行调用 getInstance() 的时候,就会创建多个实例。也就是说在多线程下不能正常工作。
懒汉式,线程安全
为了解决上面的问题,最简单的方法是将整个 getInstance() 方法设为同步(synchronized)。
1
2
3
4
5
6
public static synchronized Singleton getInstance () {
if (instance == null ) {
instance = new Singleton();
}
return instance;
}
虽然做到了线程安全,并且解决了多实例的问题,但是它并不高效。因为在任何时候只能有一个线程调用 getInstance() 方法。但是同步 *** 作只需要在第一次调用时才被需要,即第一次创建单例实例对象时。这就引出了双重检验锁。
双重检验锁
双重检验锁模式(double checked locking pattern),是一种使用同步块加锁的方法。程序员称其为双重检查锁,因为会有两次检查 instance == null ,一次是在同步块外,一次是在同步块内。为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。
1
2
3
4
5
6
7
8
9
10
public static Singleton getSingleton () {
if (instance == null ) { //Single Checked
synchronized (Singletonclass) {
if (instance == null ) { //Double Checked
instance = new Singleton();
}
}
}
return instance ;
}
这段代码看起来很完美,很可惜,它是有问题。主要在于 instance = new Singleton() 这句,这并非是一个原子 *** 作,事实上在 JVM 中这句话大概做了下面 3 件事情。
给 instance 分配内存
调用 Singleton 的构造函数来初始化成员变量
将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
我们只需要将 instance 变量声明成 volatile 就可以了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton {
private volatile static Singleton instance; //声明成 volatile
private Singleton (){}
public static Singleton getSingleton () {
if (instance == null ) {
synchronized (Singletonclass) {
if (instance == null ) {
instance = new Singleton();
}
}
}
return instance;
}
}
有些人认为使用 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。
相信你不会喜欢这种复杂又隐含问题的方式,当然我们有更好的实现线程安全的单例模式的办法。
饿汉式 static final field
这种方法非常简单,因为单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。
1
2
3
4
5
6
7
8
9
10
public class Singleton {
//类加载时就初始化
private static final Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance (){
return instance;
}
}
这种写法如果完美的话,就没必要在啰嗦那么多双检锁的问题了。缺点是它不是一种懒加载模式(lazy initialization),单例会在加载类后一开始就被初始化,即使客户端没有调用 getInstance()方法。饿汉式的创建方式在一些场景中将无法使用:譬如 Singleton 实例的创建是依赖参数或者配置文件的,在 getInstance() 之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。
静态内部类 static nested class
我比较倾向于使用静态内部类的方法,这种方法也是《Effective Java》上所推荐的。
1
2
3
4
5
6
7
8
9
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance () {
return SingletonHolderINSTANCE;
}
}
这种写法仍然使用JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。
枚举 Enum
用枚举写单例实在太简单了!这也是它最大的优点。下面这段代码就是声明枚举实例的通常做法。
1
2
3
public enum EasySingleton{
INSTANCE;
}
我们可以通过EasySingletonINSTANCE来访问实例,这比调用getInstance()方法简单多了。创建枚举默认就是线程安全的,所以不需要担心double checked locking,而且还能防止反序列化导致重新创建新的对象。但是还是很少看到有人这样写,可能是因为不太熟悉吧。
总结
一般来说,单例模式有五种写法:懒汉、饿汉、双重检验锁、静态内部类、枚举。上述所说都是线程安全的实现,文章开头给出的第一种方法不算正确的写法


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/yw/13391757.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-07-27
下一篇 2023-07-27

发表评论

登录后才能评论

评论列表(0条)

保存