如何正确的写一个单例?

      单例作为一个经典的设计模式,已经耳熟能详。它能够保持全局只保留一个对象访问点。从而可以节约系统资源,加速对象访问。我们现在讨论下如何写出一个在一个最简单的懒汉式单例的写法,如下:

public class Sigleton {
    private static Sigleton instance = null;

    private Sigleton() {
        // doSomething
    }

    public static Sigleton getInstance() {
        if (instance == null) {
            instance = new Sigleton();
        }

        return instance;
    }
}

上面是一个简单的懒汉式的单例写法:私有化构造器。获得实例的时候判断,如果实例已经被构造直接返回构造成功的实例,从而实现单例。

有过一定在并发环境下编程经验的人,可能马上会指出,上诉代码在单线程环境下没问题。但是如果是在并发环境下就会出现线程安全的问题,有些线程可能看到一个只是部分被构造的实例,而且还可能多次构造同一个实例。那么如何保证这个单例程序是线程安全的呢?

方法一:加锁!我们很容易想到,下面的想法,既然是因为在多线程环境下造成的线程安全问题,那么我们只要做合理的同步,不就可以了么?所以我们可以在获取实例的外边加锁。

public class Sigleton {
    private static Sigleton instance = null;

    private Sigleton() {
        // doSomething
    }

    public static synchronized Sigleton getInstance() {
        if (instance == null) {
            instance = new Sigleton();
        }

        return instance;
    }
}

 那么上边这个处理方法有没有什么问题呢?如果从逻辑上看,是没问题的,它确实保证了在并发环境下的线程安全问题。但是它会引起我们在并发环境下不得不考虑的一个问题:活跃性问题。也就是这个锁的粒度太大了!每个线程过来无论对象有没有被成功构建都需要在锁上发生竞争。这就是不合理的锁同步。所以这种方法正确但是性能很差

       那么好了,锁粒度既然太大,我们同步块缩小一些是不是就好了呢,像下面这样:

public class Sigleton {
    private static Sigleton instance = null;

    private Sigleton() {
        // doSomething
    }

    public static Sigleton getInstance() {
        synchronized (Sigleton.class) {
            if (instance == null) {
                instance = new Sigleton();
            }
        }

        return instance;
    }
}

上面同步代码块从代码量上看是减少了。但是减少了线程在锁上的竞争了么?其实没起卵用。所有的线程仍然会在一进入方法就进行竞争。还是没有减少在锁上的竞争。所以说,减少锁的代码块,有时候并不能起到减少锁
竞争的作用。

 那么我们知道了,其实真正需要同步的是instance==null的情况下,需要构造instance的时候。那么我们只要对这样的代码块进行同步,是不是就可以了呢?如下代码:

public class Sigleton {
    private static Sigleton instance = null;

    private Sigleton() {
        // doSomething
    }

    public static Sigleton getInstance() {
        if (instance == null) {
            synchronized (Sigleton.class) {
                instance = new Sigleton();
            }
        }

        return instance;
    }
}

上面这段代码虽然用了同步,但是它同步的位置是不正确的,依然不能解决有些线程可以看到不完整的构造实例以及实例重复构造的问题。

上面的代码其实避免了一些无谓的锁竞争,只有instance==null的情况下才会发生锁竞争。它会重复构造实例的原因是多个线程同时都满足instance==null的情况下,都进行synchronized锁的竞争,但是依然都会进行实例的构建。那么像下面这样写可不可以呢?

public class Sigleton {
    private static Sigleton instance = null;

    private Sigleton() {
        // doSomething
    }

    public static Sigleton getInstance() {
        if (instance == null) {
            synchronized (Sigleton.class) {
                if (instance == null) {
                    instance = new Sigleton();
                }
            }
        }

        return instance;
    }
}

上面这段代码我又加入了一个if判断,只有在instance == null的情况下,我才进行实例的构造。那么我是不是既减少了在锁上的竞争,又保证了线程安全呢?
其实这里我们就引入了一个著名的问题:DCL(Double Check Lock)双重检查加锁。表面上一切看起来都很好,既有效减少了在锁上的竞争,又保证了只构造一次实例。但是它避免不了有些线程可以看到无效的状态值的
问题。

方法二:DCL+Volatile

       DCL可以有效减少锁的竞争,并且能够保证对象只被构造一次,但是不能保证有些线程可以看到实例的无效状态。那么我们知道看到错误的对象状态,是由于java内存模型中可见性导致线程之间的状态不能同步。我们知道使用volatile可以保证不同线程之间的内存可见性。那么我们可以用volatile结合上边的代码写出一个性能不错,并且正确的单例。

public class Sigleton {
    private static volatile Sigleton instance = null;

    private Sigleton() {
        // doSomething
    }

    public static Sigleton getInstance() {
        if (instance == null) {
            synchronized (Sigleton.class) {
                if (instance == null) {
                    instance = new Sigleton();
                }
            }
        }

        return instance;
    }
}

方法三:利用jvm自身的机制,去确保线程安全。具体说来,就是jvm在初始化一个静态作用域的时候,是提供了额外的线程安全保证的,静态初始化器是由jvm在类的初始化阶段执行,即在类被加载后,并且被线程使用之前执行,jvm在初始化期间会获取一个锁,并且每个线程在执行的时候都会去获取一次这个锁确保这个类被加载。因此在静态初始化期间,内存写入操作自动对所有线程可见。因此无论是在被构造期间还是被引用时,静态的对象都不需要进行额外的同步

public class Sigleton {
    private static  Sigleton instance = new Sigleton();

    private Sigleton() {
        // doSomething
    }

    public static Sigleton getInstance() {
        return instance;
    }
}
其实饥饿式的单例就可以很好的保证在并发环境下的线程安全问题。

方法四:枚举。所谓单例,就是一个类只能实例化一个对象。其实使用枚举实现的单例能够保证

  • 保证只有一个实例(即使使用反射机制也无法多次实例化一个枚举量)
  • 线程安全。
public enum Sigleton {
        INSTANCE;
    private Sigleton() {
        // doSomething
    }

    public void doSomething() {
        // doSomething
    }
}

 

猜你喜欢

转载自study-a-j8.iteye.com/blog/2368661