- 目录
- 一.什么是单例?
- 二.有几种?
- 三.应用场景
- 四.注意的地方
一.什么是单例?
单例模式 保证一个类在内存中只有一个实例(对象),并提供一个访问它的全局访问点。
通常我们可以让一个全局变量使得一个对象被访问,但它不能防止你实例化多个对象。一个最好的办法就是,让类自身负责保存它的唯一实例。这个类可以保证没有其他实例可以被创建,并且它可以提供一个访问该实例的方法 —— 大话设计模式--第21章
二.单例有几种?
具体区分为下面几种:
- 饿汉式
-
懒(饱)汉式
- 线程不安全(单线程下操作的)
-
线程安全
- 方法加锁式
- 双重判断加锁式DCL(Double-Check Locking)--- 方法加锁式加强版
- 静态嵌套类式(推荐使用)
- 枚举式(推荐使用)
- 使用容器单例
不和你多bb ,上代码
1.饿汉式
public class Signleton{
//对于一个final变量。
// 如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;
// 如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。
private final static Signleton instance = new Signleton();
private Signleton(){
}
public static Signleton getInstance(){
return instance;
}
}
解释: 拿时间换空间,因为实例对象在类加载过程中就会被创建,在getInstance()方法中只是直接返回对象引用。因为这种创建实例对象方式比较‘急’,所以称为饿汉式。
优点:快很准,无需关心线程安全问题。
缺点:
1.无论对象会不会被使用,在类加载的时候就创建对象了,这样会降低内存的使用率。
2.如果在一个大环境下使用了过多的饿汉单例,则会生产出过多的实例对象,无论你是否要使用他们
2.懒(饱)汉式
(1)线程不安全(单线程下操作的)
public class Signleton{
private static Signleton instance = null;
private Signleton(){
}
public static Signleton getInstance(){
if(instance==null){
instance = new Signleton();
}
return instance;
}
}
解释: Singleton的静态属性instance中,只有instance为null的时候才创建一个实例,构造函数私有,确保每次都只创建一个,避免重复创建。之所以被称为“懒汉”,因为它很懒,不急着生产实例,在需要的时候才会生产。
优点:延迟加载
缺点:只在单线程的情况下正常运行,在多线程的情况下,就会出问题。例如:当两个线程同时运行到判断instance是否为空的if语句,并且instance确实没有创建好时,那么两个线程都会创建一个实例。
(2)线程安全
方法加锁式
public class Signleton{
private static Signleton instance = null;
private Signleton(){
}
//给方法加锁 若有ABCD线程使用 A线程先进入 BCD线程都需要等待A线程执行完毕释放锁才能获得锁执行该方法
//这样效率较低
public syschronized static Signleton getInstance(){
if(instance==null){
instance = new Signleton();
}
return instance;
}
}
解释:给方法添加[synchronized],使之成为同步函数。两个线程同时想创建实例,由于在一个时刻只有一个线程能得到同步锁,当第一个线程加上锁以后,第二个线程只能等待。第一个线程发现实例没有创建,创建之。第一个线程释放同步锁,第二个线程才可以加上同步锁,执行下面的代码。由于第一个线程已经创建了实例,所以第二个线程不需要创建实例。保证在多线程的环境下也只有一个实例。
优点: 保证了线程安全。延时加载,用的时候才会生产对象。
缺点: 需要保证同步,付出效率的代价。加锁是很耗时的。。。
双重判断加锁式DCL
public class Signleton {
private static Signleton instance = null;
private Signleton(){
}
//假设有ABCD 个线程 使用这个方法
public static Signleton getInstance(){
//BCD都进入了这个方法
if(instance==null){
//而A线程已经给第二个的判断加锁了
syschronized(Signleton.class){
//这时A挂起,对象instance还没创建 ,故BCD都进入了第一个判断里面,并排队等待A释放锁
//A唤醒继续执行并创建了instance对象,执行完毕释放锁。
//此时到B线程进入到第二个判断并加锁,但由于B进入第二个判断时instance 不为null了 故需要再判断多一次 不然会再创建一次实例
if(instance==null){
instance = new Signleton();
}
}
}
return instance;
}
}
解释:方法加锁式的优化。只有当instance为null时,需要获取同步锁,创建一次实例。当实例被创建,则无需试图加锁。。
优点: 保证了线程安全。延时加载,用的时候才会生产对象。进行双重判断,当已经创建过实例对象后就无需加锁,提高效率。
缺点: 编写复杂、难记忆。虽然是优化加锁式,但加锁始终会耗时。
3.静态嵌套类式(推荐使用)
public class Signleton{
private Signleton{
}
//静态嵌套类 这里给个链接 区分静态嵌套类和内部类[静态嵌套类和内部类](http://blog.csdn.net/iispring/article/details/46490319)
private static class SignletonHolder{
public static final Signleton instance = new Signleton();
}
public static Signleton getInstance(){
return SignletonHolder.instance;
ins't
}
}
解释: 定义一个私有的静态嵌套类,在第一次用这个嵌套类时,会创建一个实例。而类型为SingletonHolder的类,只有在Singleton.getInstance()中调用,由于私有的属性,他人无法使用SingleHolder,不调用Singleton.getInstance()就不会创建实例。
优点: 保证了线程安全。无需加锁,延迟加载,第一次调用Singleton.getInstance才创建实例。推荐使用。
缺点: 无。
4.枚举式(推荐使用)
为了方便理解枚举式 这边简单介绍一下枚举(在jdk1.5后)
//枚举Type
public enum Type{
A,B,C;
private String type;
Type(type){
this.type = type;
}
public String getType(){
return type;
}
}
//可认为等于下面的
public class Type{
public static final Type A=new Type(A);
public static final Type B=new Type(B);
public static final Type C=new Type(C);
ins't
}
所以Type.A.getType()为A.
推荐去了解一下
Java学习整理系列之Java枚举类型的使用
Java学习整理系列之Java枚举类型的原理
好了 开始介绍枚举式了 看代码
public class Signleton{
public static Signleton getInstance(){
return SignletonEnum.INSTANCE.getInstance();
}
public enum SignletonEnum{
INSTANCE;
private Signleton instance;
//由于JVM只会初始化一次枚举实例,所以instance无需加static
private SignletonEnum(){
instance = new Signleton();
}
public getInstance(){
return instance;
}
}
}
解释: 定义内部的枚举,由于类加载时JVM只会初始化一次枚举实例,所以在构造函数中创建Signgleton对象并保证了这个对象实例唯一。
通过调用枚举INSTANCE方法getInstance (SignletonEnum.INSTANCE.getInstance())获取实例对象。
优点: 枚举提供了序列化机制--例如在我们要通过网络传输一个数据库连接的句柄,会提供很多帮助。
单元素的枚举类型已经成为实现Singleton的最佳方法。Effective Java
5.使用容器单例
public class Singleton {
//用Map保存该单例
private static Map<String, Object> objMap = new HashMap<>();
private Singleton() {
}
public static void putObject(String key, String instance){
if(!objMap.containsKey(key)){
objMap.put(key, instance);
}
}
public static Object getObject(String key){
return objMap.get(key);
}
}
解释: 在程序开始的时候将单例类型注入到一个容器之中 ,在使用的时候再根据 key 值获取对应的实例,这种方式可以使我们很方便的管理很多单例对象,也对用户隐藏了具体实现类,降低了耦合度。
缺点: 会造成内存泄漏。(所以我们一般在生命周期销毁的时候也要去销毁它) 。
三.应用场景
一般创建一个对象需要消耗过多的资源,如:访问I0和数据库等资源或者有很多个地方都用到了这个实例。
四.注意的地方
双重判断加锁式DCL :这种写法也并不是保证完全100%的可靠,由于 java 编译器允许执行无序,并且 jdk1.5之前的jvm ( java 内存模型)中的 Cache,寄存器到主内存的回写顺序规定,第二个和第三个执行是无法保证按顺序执行的,也就是说有可能1-2-3也有可能是1-3-2; 这时假如有 A 和 B 两条线程, A线程执行到3的步骤,但是未执行2,这时候 B 线程来了抢了权限,直接取走 instance 这时候就有可能报错。
简单总结就是说jdk1.5之前会造成两个问题:
1、线程间共享变量不可见性;
2、无序性(执行顺序无法保证);
当然这个bug已经修复了,SUN官方调整了JVM,具体了Volatile关键字,因此在jdk1.5之前只需要写成这样既可, private Volatitle static Singleton instance; 这样就可以保证每次都是从主内存中取,当然这样写或多或少的回影响性能,但是为了安全起见,这点性能牺牲还是值得。