单例模式

🥇单例模式(Singleton)

很多时候,我们在系统中使用的对象有且仅仅需要一个,如果大量的实例化反而会产生性能问题或其他的不必要的麻烦。在我们的日常使用中有很多单例情况,比如说 windows 操作系统的资源管理器,有且只有一个资源管理器可以被使用。

如何设计该类,使其只能产生一个实例?我们必须保证这个类不能有多余的实例。

我们都知道在进行类的实例的时候,必须使用类构造器来进行,如果我们将类构造器设置为私有(private),禁止外部进行实例化时,便可以很方便的解决该问题。

我们将构造器私有化,在类的内部进行实例,并且进行返回。这样便形成了我们的单例,确保了类中间只有一个实例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Singleton private constructor() {
    companion object {
        private var instance: Singleton? = null
        fun getInstance(): Singleton {
            if (instance == null) {
                instance = Singleton()
            }
            return instance!!
        }
    }
}

这样我们的类就形成了一个单例。

如上的写法是单例模式中的懒汉模式,为什么叫懒汉模式,因为该对象在使用的时候才会初始化。

但是上述的写法存在一定的问题。当线程 A 访问该方法时,代码执行到 getInstance 还没有进行判断或者实例化的时候,线程 B 也开始进行访问该方法。那么此时线程A就会进行一次实例化,线程 B 也会进行一次实例化。此刻就会出现问题,单例不再单例,两个线程创建的实例并不是同一个实例。

为了解决上述问题,我们可以对 getInstance() 进行加锁。确保同一时刻只能由一个线程访问getInstance 方法。这样便解决了线程同步问题。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Singleton private constructor() {
    companion object {
        private var instance: Singleton? = null
        @Synchronized
        fun getInstance(): Singleton {
            if (instance == null) {
                instance = Singleton()
            }
            return instance!!
        }
    }
}

通过 @Synchronized 标记该方法为一个同步方法。

通过 @Synchronized 注解解决了多线程访问问题,但是随之而来的又出现了性能问题。如果线程 A 在执行 getInstance 方法的时候,线程 B 就必须等待。反之亦然。如果是一个高并发系统,那么就会带来巨大的性能损耗,总是有一堆线程在等待中。

面对上述的问题,那么就应该做出新的改变。公共写是危险操作,而公共读确实安全的。所以我们在进行创建的时候加锁,而不是在整个方法上。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Singleton private constructor() {
    companion object {
        private var instance: Singleton? = null

        fun getInstance(): Singleton {
            if (instance == null) {
                synchronized(Singleton::class.java) {
                    if (instance == null) {
                        instance = Singleton()
                    }
                }
            }
            return instance!!
        }
    }
}

这里在加锁前和加锁后都进行了判断,通过这样的双检测来解决了功能上单例问题和性能上问题。

如果加锁后不检查会发生什么?

如果加锁后不检查,那么假设两个线程 A、B 同时进入了 getInstance 并且都执行到加锁前,如果不进行判断,那么只要其中一个线程申请了加锁,那么另外一个就要等待,当前一个释放锁后,后面的就会加锁继续实例化。此时又会产生两个不同的实例。而加锁后判断就避免了这样问题。

然而在 kotlin 中提供了更简单实现方式,通过 by lazy 来实现双检查锁。

1
2
3
4
5
class Singleton private constructor() {
    companion object {
        val instance: Singleton by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { Singleton() }
    }
}

对于单例的另一个模式那就是饿汉模式。而饿汉模式的名字由来是因为饿了就需要吃,所以提前就都准备好(初始化)。因为已经提前都初始化好了,所以也不存在上述的线程安全问题。

1
object Singleton

单例模式和其他创建型模式不同,单例更重要的是解决多次创建带来的性能上的问题。

相关内容