单例模式是23中设计模式中最简单的一种,只需要一个类就可以实现,是非常常用的设计模式之一。老司机开车,请扶好坐稳,前方到站-单例模式站。
1、单例类只能有一个实例;
2、单例类必须自己创建自己唯一的实例;
3、单例对象必须给其他所有对象提供这一实例;
1、只有一个对象,内存开至少、性能好;
2、避免对资源的多重占用;
3、在系统设置全局访问点,优先和共享资源访问。
1、私有化构造器(可以防止外部使用创建对象的方式调用该类中的方法);
2、定义一个类方法用于获得单例对象,返回值是这个类的对象;
3、在类中定义一个singleton类型的类属性;
4、定义一个实现类方法(在本类中使用的是getInstance()方法)。
单例模式的写法有饿汉式、懒汉式、双重校验锁、静态内部类、枚举五种类型,后续我将对这五种写法的优点、缺点以及实现方式一一进行介绍。下面正式发车……
饿汉式采用了一种简单粗暴地方式,在类加载的时候就对对象进行了初始化,代码如下:
public class Singleton {
/**
* 饿汉式单例
*
* 优点:类加载的时候创建一次实例,避免了多线程同步问题;
* 缺点:即使单例没被用到也会创建,浪费内存
*/
//3、在类中定义一个Singleton类型的类属性
private static Singleton singleton = new Singleton();
//1、私有化构造器
private Singleton() {
}
//2、定义一个类方法用于获得单例对象,返回值是这个类的类型
public static Singleton getInstance() {
return singleton;
}
}
由于饿汉式单例有一个很大的缺点,就是即使没有被用到也会创建对象,造成资源浪费,然后就产生了饿汉式单例的变种。代码如下:
public class Singleton{
/**
* 饿汉式单例-变种
*
* 将创建对象放在静态代码块中,只有在使用单例类的时候才会创建对象,同时也保证了线程安全
*/
private static Singleton singleton = null;
static {
singleton = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
return singleton;
}
}
懒汉模式是一种偷懒的模式,只有在使用单例类的时候才会去创建对象。解决饿汉式单例资源浪费的问题,但同时也产生了新的问题。代码如下:
public class Singleton{
/**
* 懒汉式单例(非线程安全)
*
* 优点:需要时才会去创建实例,节省资源
* 缺点:没有考虑线程安全问题,多个线程并发调用getInstance,可能会创建多个实例
*/
private static Singleton singleton = null;
private Singleton(){
}
public static Singleton getInstance(){
if(null == singleton){
singleton = new Singleton();
}
return singleton;
}
}
懒汉式单例可以再需要的时候就去创建对象,节省资源空间。但是如果两个线程同时到达if(null == singleton)处时,两个线程的singleton都是null,所以都会进入到方法体中去创建对象,此时就会出现两个不同的对象,这时这个单例就失去了它的作用。别急,有了问题自然就会有解决办法,这不懒汉式单例的变种就应运而生了。
public class Singleton{
/**
* 懒汉式单例-变种(线程安全)
*
* 缺点:性能问题,添加了synchronized的函数比一般方法慢得多,若多次调用getInstance,则累积的
* 性能损耗特别大
*/
private static Singleton singleton;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if(singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
我们再来讨论一下懒汉式单例加锁的问题,对于getInstance()方法,绝大多数的操作都是读操作,读操作基本上都是线程安全的,我们没必要让每个线程必须持有锁才能调用该方法,所以我们需要调整加锁的问题。由此也就产生了一种新的模式:双重校验锁模式。代码如下:
public class Singleton{
/**
* 双重校验锁
*
* 注意:volatile的一个语义是禁止指令重排序优化,也就保证了instance变量被赋值的时候对象已经
* 是初始化过的(jdk1.5之后支持)
*/
private static volatile Singleton singleton = null;
private Singleton(){
}
public static Singleton(){
if(null == singleton){
synchronized(Singleton.class){
if(null == singleton){
singleton = new Singleton();
}
}
}
return singleton;
}
}
双重校验锁模式是一种非常好的单例模式,解决了单例、性能、线程安全问题。上面的双重校验锁模式如果不加volatile关键字,就还是存在问题的。在多线程的情况下,可能会出现空指针问题。出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。那么什么是指令重排序呢?看下面一个小栗子:
public class Test{
public void test1(){
1 int x = 10;
2 int y = 11;
3 String singleton = "sing";
}
}
上面的方法,我们在编写的时候是按1、2、3的顺序进行的,但是JVM会对它进行指令重排序,所以执行顺序可能是3、2、1或1、3、2等,不管是哪种执行顺序,JVM都会保证所有实例进行实例化。如果方法中操作比较多时,为了提高效率,JVM会在方法里边的属性未全部实例化时就返回对象。当某个线程获取锁进行实例化时,其他线程就直接获取实例使用,由于JVM指令重排序的原因,其他线程获取到的也许不是一个完成的对象,所以在使用实例的时候就会出现空指针异常的问题。
要解决双重校验锁模式带来的空指针异常问题,只需要使用volatile关键字,volatile关键字严格遵循happen-before原则,即在读操作前,写操作必须全部完成。
静态内部类单例模式又称单例持有者模式,实例由内部类创建,由于JVM在加载外部类的过程中,是不会加载静态内部类的,只有内部类的属性/方法被调用时才会被加载,并初始化其静态属性。静态属性又static修饰,保证只被实例化一次,并且严格保证实例化顺序。
public class Singleton{
/**
* 静态内部类
*
* 优点:既实现了线程安全,又省去了null的判断
*/
private Singleton(){
}
private static class SingletonHolder(){
public static Singleton singleton = new Singleton();
}
public static Singleton getSingleton(){
return SingletonHolder.singleton;
}
}
静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种设计模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。
枚举类单例模式是effective java作者极力推荐的单例设计模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分地利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是是所有单例实现中唯一不会被破坏的单例实现模式。代码如下:
public class Singleton{
private Singleton(){
}
/**
* 枚举类型是线程安全的,并且只会装载一次
*/
private enum SingletonEnum{
INSTANCE;
private final Singleton instance;
SingletonEnum(){
instance = new Singleton();
}
private Singleton getInstance(){
return instance;
}
}
public static Singleton getInstance(){
return Singleton.INSTANCE.getInstance();
}
}
1、除枚举方式外,其他方法都会通过反射的方式破坏单例,反射是通过调用构造方法生成新的对象,所以如果我们想要组织单例破坏,可以再构造方法中进行判断,若已有实例,则阻止生成新的实例,解决方法如下:
private Singleton(){
if(null == singleton){
throw new RuntimeException("实例已经存在,请通过getInstance()方法获取");
}
}
2、如果单例类实现了序列化接口Serializabale,就可以通过反序列化破坏单例,所以我们可以不实现序列化接口,如果非得实现序列化接口,可以重写反序列化方法readResolve(),反序列化时直接返回先关返利对象。
public Object readResolve() throws ObjectStreamException{
return singleton;
}
好了,单例模式站已经到达,各位乘客下一站代理模式站见哦~