倒霉的菜鸟

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

单例模式属于创建型模式, 顾名思义,就是说整个系统中只有一个该对象的实例。

为什么要使用单例模式?

1, 对于一些需要频繁创建,销毁的对象, 使用单例模式可以节省系统资源

2, 对于全局持有的对象,单例模式方便控制

实现单例模式的关键?

1, 构造方法私有,阻止对象被外部随意创建

2, 以静态方法返回实例, 因为无法从外部创建,所以要提供静态方法返回实例

3, 确保实例只有1个, 只对类进行一次实例化,以后都取第一次实例化的对象

单例模式常见写法

  • 饿汉式
public class EHan {

//类加载时实例化对象
private static EHan instance = new EHan();

//构造方法私有
public EHan() {

}
//提供静态方法供外部调用返回该类的实例
public static EHan getInstance() {
return instance;
}

public void eat(){
System.out.println("eat...");
}
}
饿汉式单例就是在类初始化时就实例化对象,后面要用的时候直接拿去用就行。 它的优点是线程安全,没有加锁,执行效率高。缺点是没有懒加载,如果这个类自始至终没有真正使用过,那就是浪费了内存
那么饿汉式单例在kotlin中怎么写呢? 我们直接使用studio的一键转换功能,得到如下代码
class EHan {
fun eat() {
println("eat...")
}

companion object {
val instance = EHan()
}
}
其实在kotlin中还可以写得更简单:
object EHan {
fun eat() {
println("eat...")
}
}
一个object关键字就申明了饿汉式单例, 为了证明, 我们可以通过studio查看上述代码的字节码
tools--> kotlin--> show kotlin Bytecode. --> DeCompile 得到结果如下

这是因为在kotlin中,类没有静态方法. object关键字在kotlin中有两种使用场景, 对象表达式和对象声明。对象声明就是在kotlin中声明单例的方式

上面的companion object也是一样的道理, 不同的是 伴生对象在整个类中只能有1个

  • 懒汉式

懒汉式分为线程安全的和线程不安全的, 我们先来看不安全的

public class LanHan {

private static LanHan instance;

//构造方法私有
private LanHan(){

}

//提供静态方法返回实例
public static LanHan getInstance(){
//如果为空就去实例化, 否则返回instance
if (instance == null) {
instance = new LanHan();
}
return instance;
}
}
它对应的在kotlin中的写法如下:
class LanHan private constructor() {
companion object {
private var instance: LanHan? = null
get() {
if (field == null) {
field = LanHan()
}
return field
}

fun get(): LanHan {
return instance!!
}
}
}

这种写法,如果某个线程执行到if (instance == null) 还没来得及往下执行时,另一个线程也执行到了这里,那么就会创建两个实例, 所以是线程不安全的。
那么如何让它线程安全呢?答案是synchronized关键字
public class LanHan {

private static LanHan instance;

//构造方法私有
private LanHan(){

}

//提供静态方法返回实例
public static LanHan getInstance(){
//加锁, 防止多个线程同时执行实例化
synchronized (LanHan.class){
//如果为空就去实例化, 否则返回instance
if (instance == null) {
instance = new LanHan();
}
}
return instance;
}
}
在kotlin中的写法:
class LanHan private constructor() {
companion object {
private var instance: LanHan? = null
get() {
if (field == null) {
field = LanHan()
}
return field
}

@Synchronized
fun get(): LanHan {
return instance!!
}
}
}
在kotlin中,@Synchronized注解表明给该方法加同步锁
加锁之后, 实现了线程安全, 多个线程执行到这里时要排队等待, 这又严重影响了性能, 怎么解决呢?

方案一: 双重锁(DCL)
所谓双重锁就是判断两次if(instance == null), 只有等于null时才上锁
public static LanHan getInstance(){
//只有对象未创建时才上锁
if (instance == null){
//加锁, 防止多个线程同时执行实例化
synchronized (LanHan.class){
//如果为空就去实例化, 否则返回instance
if (instance == null) {
instance = new LanHan();
}
}
}
return instance;
}

DCL是完美的解决了单例模式中性能和资源浪费的问题,但是DCL在并发情下也会存在一个问题,因为Jvm指令是乱序的;

情况如下:

线程1调用getInstance 获取对象实例,因为对象还是空未进行初始化,此时线程1会执行new Singleton()进行对象实例化,而当线程1的进行new Singleton()的时候JVM会生成三个指令。

指令1:分配对象内存。

指令2:调用构造器,初始化对象属性。

指令3:构建对象引用指向内存。

 

因为编译器会自作聪明的对指令进行优化, 指令优化后顺序会变成这样:

1、执行指令1:分配对象内存,

2、执行指令3:构建对象引用指向内存。

3、然后正好这个时候CPU 切到了线程2工作,而线程2此时也调用getInstance获取对象,那么线程2将执行下面这个代码 if (singleton == null),此时线程2发现对象不为空(因为线程1已经创建对象引用并分配对象内存了),那么线程2会得到一个没有初始化属性的对象(因为线程1还没有执行指令2)。

所以在这种情况下,双检测锁定的方式会出现DCL失效的问题。

那么双重锁在kotlin代码里怎么写呢, 我们先直接一键转换看一下

object LanHan {
var instance: LanHan? = null
get() {
if (field == null) {
synchronized(LanHan::class.java) {
if (field == null) {
field = LanHan
}
}
}
return field
}
private set
}
看起来是不是很messy, 有没有更优雅的写法呢, 百度了一下果然有
class LanHan private constructor() {
companion object {
val instance: LanHan by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
LanHan()
}
}
}
这个看起来就简洁多了, 这里有个知识点就是lazy
Lazy是接受一个 lambda 并返回一个 Lazy 实例的函数,返回的实例可以作为实现延迟属性的委托:第一次调用 get() 会执行已传递给 lazy() 的 lambda 表达式并记录结果, 后续调用 get() 只是返回记录的结果。

(摘自 什么是单例模式_东非大裂谷的博客-CSDN博客_什么是单例模式

方案二: 静态内部类 

public class LanHan {

//构造方法私有
private LanHan(){}

//静态内部类
private static class LanHanHolder{
//内部类第一次访问时创建对象实例
private static LanHan instance = new LanHan();
}

public static LanHan getInstance(){
return LanHanHolder.instance;
}
}
因为当外部类被访问时并不会加载内部类, 所以只有在访问LanHanHolder这个内部类时才会去实例化对象,这就实现了懒加载的效果.但是静态内部类的方式只适用于静态域
可以看到单例模式的各种写法里都用到了static关键字, 这是因为static修饰的语句/代码块在整个类加载过程中只执行一次。

那么静态内部类在kotlin中怎么写呢?
class LanHan private constructor() {

private object LanHanHolder {
val holder = LanHan()
}

companion object{
val instance = LanHanHolder.holder
}
}
  • 枚举
public enum MeiJu {
INSTANCE;
public void run(){
}
}
这是一种非常简单的单例实现, 避免了线程同步问题,支持自动序列化机制,且绝对不会多次实例化
枚举的特性:
1, 它不能有public的构造函数, 这样就保证了其他代码没有办法新建一个enum实例。
2,所有枚举值都是public, static, final 的
3, Enum默认实现了jave.lang.comparable接口
4, Enum覆载了toString方法
5, Enum提供了valueOf方法, 用于从string中读取枚举类型
6, Enum提供了values方法, 用来遍历所有的枚举值
枚举在kotlin中的写法和java大同小异, 不再赘述
enum class MeiJu {
INSTANCE;
fun run() {}
}

(kotlin部分参考 Kotlin下的5种单例模式,看了都说好!_程序员小乐-CSDN博客 感谢博主)


posted on 2021-09-06 22:05  倒霉的菜鸟  阅读(864)  评论(0编辑  收藏  举报