设计模式之单例模式

单例模式是一种常用的软件设计模式,定义:单例对象的类只能允许一个实例存在(可以理解一个国家只能有一个主席)。

在此之前回忆一下类的加载过程

在这里插入图片描述

一般有下面几种操作会触发类的初始化

new: new一个对象的时候
访问静态属性的时候
调用静态方法的时候
进行反射调用的时候
初始化一个类时父类还没初始化 则初始化父类
带有main方法的类
使用jdk动态语言支持时 可参考别人的这篇文章
例外:在类里面定义数组引用不会触发初始化

  1. 加载:将class二进制文件读入内存,放在运行时的数据区的方法区内,然后再堆中创建一个Class对象,用来封装方法区的数据结构
  2. 连接:分为验证(验证类文件的结构,语义的合法性,final修饰的类是否有子类,final修饰的方法是否被重写等), 准备(为类的静态变量分配内存空间并赋默认值) 和解析阶段(符号引用替换为直接引用 也就是将字段方法的名称替换为内存地址 为了支持java语言的运行时绑定可以在初始化阶段之后再开始),
  3. 初始化:为类的静态变量赋初始值 java虚拟机才真正开始执行类定义中的程序代码

1.饿汉式
这种方式在类加载时的初始化阶段的时候实例就创建好了,问题是,如果创建的这个对象不受控制,假如(别的静态方法导致类初始化)而该对象一直都没有使用,那无疑是种浪费 new了又从来没用

public class Singleton {
	private static Singleton instance=new Singleton();
	private Singleton(){
	}
	public static Singleton getInstance(){
		return instance;
	}
}

2.synchronized方法
下面这种方式恐怕是每个人都能第一时间想到的实现方式

public class Singleton {
	private static Singleton instance;
	private Singleton(){
		
	}
	public static synchronized Singleton getInstance(){
		if(instance==null){
			instance=new Singleton();
		}
		return instance;
	}
}
由于在静态方法上加锁-类锁,效率不高

3.使用 静态内部类

public class Singleton {
	private Singleton(){
		
	}
	private static class SingletonHolder{
		private static Singleton instance=new Singleton();
	}
	public static Singleton getInstance(){
		return SingletonHolder.instance;
	}
}

利用jvm classloder机制来保证初始化instance时只有一个线程,和饿汉式的区别是:饿汉式只要Singleton类被加载,那么instance就会被实例化,而这种方式是Singleton类被加载了,instance也不一定被初始化,只有通过调用外部类的getInstance()方法时才会显式装载SingletonHolder类,从而实例化instance,可以做到需要的时候创建实例

4.枚举

public class Singleton {

	private enum MyEnumSingleton{
		singletonFactory;
		private Singleton instance;
		//枚举类的构造方法在类加载时被实例化
		private MyEnumSingleton(){
			instance=new Singleton();
		}
		public Singleton getInstance(){
			return instance;
		}
	}
	
	public static Singleton getInstance(){
		return MyEnumSingleton.singletonFactory.getInstance();
	}
}

更为简洁的写法
public enum Singleton {  
    INSTANCE;  
    
    public void method() {  
    }  
} 

枚举的本质就是 实例可数, 上面创建两个实例,一个singletonFactory, instance, 利用工厂模式来获取
这种方式不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象

5.static 代码块 这个和静态变量一样,在使用类初始化的时候就已经执行赋值

public class Singleton {
	private static Singleton instance=null;
	private Singleton(){
	}
	static {
		instance=new Singleton();
	}
 
	public static Singleton getInstance(){
		return instance;
	}
}

6.双重检查


public class Singleton {
	private volatile static Singleton instance=null;
	private Singleton(){
		
	}
 
	public static Singleton getInstance(){
		if(instance==null){    ----------------------1
			synchronized(Singleton.class){
				if(instance==null){------------------2
					instance=new Singleton();
				}
			}
		}
		return instance;
	}
}

多线程环境时,如果两个线程都停留在1处, 可以想象没有2处的校验的话, 第二个线程等待第一个线程释放锁后,又实例化了一遍。理论上是线程安全的
“双重检查的问题是:并不能保证它会在单处理器或多处理器计算机上顺利运行。双重检查失败的问题并不归咎于 JVM 中的实现 bug,而是归咎于 Java 平台内存模型。内存模型允许所谓的“无序写入” 导致二次检查失败,此法也就行不通了”

关于java 内存模型的无序写入

对象的创建的过程: Singleton s = new Singleton()
1、给Singleton的实例分配内存空间。

2、调用Singleton的构造方法进行构造函数初始化

3、将instance对象指向分配的内存空间
  但是由JVM的乱序执行, 上面1、2、3的执行顺序中2和3并不一定,可能是1、2、3也可能是1、3、2如果是1、2、3还好说并不会出现什么问题,但是如是执行的顺序是1、3、2那么就较麻烦了,因为在执行到3的时候对象已经是非null了(只是还没被初始值来初始化),所以其线程有可能取到被***初始化到一半的对象***(对于64位数据往32位平台写,类似出现的写撕裂问题),出现这中错误的原因是instance = new Singleton();并没有真正的执行完,就释放了锁!

那么如何解决这个问题?
  我们可以使用volatile来阻止程序的乱序执行,从而使双重检验锁在多线程下正确执行。
volatile关键字是什么作用:
第一:保证被volatile修饰的变量会保证对所有的线程的可见性,这里的“ 可见性 ”是指当一条线程修改了这个变量的值,新值对于其他的线程是可以立即得知的。
第二:使用volatile变量的语意是***禁止指令重排序优化***,普通的变量仅仅会保证该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作顺序与程序代码中的执行顺序一致。
但是我们在使用volatile的同时使我们的代码不能被编译器进行代码优化,但是***需要在本地代码中插入许多的内存屏障指令来保证发生乱序执行,导致我们的程序在执行的时候变慢***
不过我看网上倒是还有一种方法解决:

public class Singleton {    
    private static Singleton singleton;
    private Singleton() {   
    }   
    public static Singleton getInstance() { 
        if (singleton == null) {    
            synchronized (Singleton.class) {
                Singleton temp = singleton;
                if (temp == null) {    
                    singleton = new Singleton();  
                }   
            }   
        }   
        return singleton;   
    }  
}

仔细发现并未解决问题, 所以该模式还是尽量别用了

//问题?
如果单例类由不同的类装载器装入,那便有可能存在多个单例类的实例。(但是利用下面的方式手动装载单例类是可以的)

原理:
setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。
如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。如果没有父线程,Java 应用运行的初始线程的上下文类加载器是系统类(应用)加载器(appClassLoader)。

打个比方,tomcat是多线程服务的,那么从当前线程(线程也是对象,也是被类加载器加载的)所用的类加载器来加载这个单例类时,如果每个线程的类加载器都不同,就有可能创建这个单例类的多个实例,一般程序里不会设置setContextClassLoader(ClassLoader cl), 此时共同调用同一个类加载器来加载单例类。
private static Class getClass(String classname) throws ClassNotFoundException {     

//此段代码要生效,最好别用setContextClassLoader(ClassLoader 手动设置线程的上下文类加载器,要设置也要保持每个线程的一致性

      ClassLoader classLoader = Thread.currentThread().getContextClassLoader();     
 
      if(classLoader == null)    
      {
         classLoader = Singleton.class.getClassLoader();     
      }
      
      return (classLoader.loadClass(classname));     
   }     
} 

2.如果Singleton实现了java.io.Serializable接口,那么这个类的实例就可能被序列化和复原。如果序列化一个单例类的对象,接下来复原那个对象,那你依然会有多个单例类的实例(不实现就行 或者提供一个readResove方法,因为当从I/O流中读取对象时,readResolve()方法都会被调用到,实际上也就是用readResolve()中返回的对象直接替换在反序列化过程中创建的对象)。

问题:如果通过反射的方式直接调用私有构造器??可以直接通过在构造器里抛出异常来解决

public class Singleton implements java.io.Serializable {     
   public static Singleton instance = new Singleton();     
   private Singleton() {     
        
   }     
   private Object readResolve() {     
        return instance;     
   }    
} 

synchronized说明

  1. 修饰方法
public synchronized void method()
{
   // todo
}

public void method()
{
   synchronized(this) {
      // todo
   }
}

以上两种写法效果一样,不过第二种可归为修饰代码块
synchronized不能被继承,即使子类覆盖了该方法,但未加synchronized修饰,仍认为是普通方法

2 修饰代码块

class MyThread implements Runnable {
	   private static int count;
	   public MyThread() {
	      count = 0;
	   }
	   public  void run() {
	      synchronized(this) {
	         for (int i = 0; i < 5; i++) {
	            try {	               System.out.println(Thread.currentThread().getName() + ":" + (count++));
	               Thread.sleep(1000);
	            } catch (InterruptedException e) {
	               e.printStackTrace();
	            }
	         }
	      }
	   }
 
	   public int getCount() {
	      return count;
	   }
}
 
public class DemoTest {
	public static void main(String args[]){
//示例1    交替执行  
//		SyncThread s1 = new SyncThread();
//		SyncThread s2 = new SyncThread();
//		Thread t1 = new Thread(s1);
//		Thread t2 = new Thread(s2);
//示例2		t1先执行完  t2再继续执行  因为锁定的为同一对象 ,注意t1执行时 t2是可以执行非同步方法的
		SyncThread s = new SyncThread();
		Thread t1 = new Thread(s);
		Thread t2 = new Thread(s);
		
		t1.start();
		t2.start();
	}
}

  1. 修饰静态方法或在类对象上加锁
public synchronized static void method()
{
   // todo
}
class ClassName {
   public void method() {
      synchronized(ClassName.class) {
         // todo
      }
   }
}
这种以及修饰静态方法的方式锁住的是这个类的所有对象

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值