Kotlin设计模式分享

单例模式

一个类有且仅有一个实例,并且自行实例化向整个系统提供.保证全局只有一个实例对象

饿汉式 - 在使用之前就创建好了

使用kotlin的关键字 object 就可以 - static类块里初始化,只加载一次

1
2
3
4
5
6
7
object Singleton{
    val bean = 1
    fun log():Int{
        val beans = bean + 1
        return beans
    }
}

Android Studio -> Tools -> Kotlin -> show Kotlin bytecode -> Decompile
反编译后看原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public final class Singleton {
   private static final int bean;
   @NotNull
   public static final Singleton INSTANCE;

   public final int getBean() {
      return bean;
   }

   public final int log() {
      int beans = bean + 1;
      return beans;
   }

   private Singleton() {
   }

   static {    // 在类块中初始化
      Singleton var0 = new Singleton();
      INSTANCE = var0;
      bean = 1;
   }
}

懒汉式 - 在使用时才创建

线程不安全

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Singleton private constructor(){
    companion object{
        private var singleton : Singleton? = null
            get() {
                if (field == null)
                    field = Singleton()
                return field;
            }
        fun get() : Singleton? {
            return singleton
        }
    }
        fun log(){
        BookLogger.i("Singleton")
    }
}

反编译为java为: 如果在单例对象创建之前,同时有两个线程要使用对象,那判断对象为null,就有可能同时创建了两个对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public final class Singleton {
   private static Singleton singleton;

@NotNull
public static final Singleton.Companion Companion = new Singleton.Companion((DefaultConstructorMarker)null);

   private Singleton() {
   }

   public Singleton(DefaultConstructorMarker $constructor\_marker) {
      this();
   }

   public static final class Companion {
      private final Singleton getSingleton() {
         if (Singleton.singleton == null) {
            Singleton.singleton = new Singleton((DefaultConstructorMarker)null);
         }

         return Singleton.singleton;
      }

      private final void setSingleton(Singleton var1) {
         Singleton.singleton = var1;
      }

      @Nullable
      public final Singleton get() {
         return (([Singleton.Companion)this).getSingleton();
      }


      private Companion() {
      }

      public Companion(DefaultConstructorMarker $constructor\_marker) {
         this();
      }
   }
}

线程安全 - 方案一: 直接给get方法加锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Singleton private constructor(){
    companion object {
        private var singleton : Singleton? = null
            get() {
                if (field == null)
                    field = Singleton()
                return field;
            }
        @Synchronized
        fun get() : Singleton? {
            return singleton
        }
    }
}

分析:只给get()方法加锁,若在构造方法里对对象进行「a++」 类似操作,可能在某个线程只创建对象,分配了内存,还没完成数据操作,另一个线程就拿到了对象「a」,可能会出现数据错误,可以使用volatile关键字限制代码指令重排解决;并且对get()方法加锁,每个线程需要拿到锁,才能对对象进行操作,比较耗时.

线程安全 - 方案二:DCL (double check lock)

方案一每次调用该方法的时候都得获取锁,但是如果这个单例已经被初始化了,其实按道理就不需要申请同步锁了,直接返回这个单例类实例即可,使用kotlin的by lazy函数,指定SYNCHRONIZED线程安全模式,这种方式可以保证「在第一次使用这个属性时执行初始化代码,并保存属性,后续访问的时候直接返回,不再执行初始化代码块」.

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

反编译成java代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public final class Singleton {
   @NotNull
   private static final Lazy Instance$delegate;
   @NotNull
public static final Singleton.Companion Companion = new Singleton.Companion((DefaultConstructorMarker)null);

   private Singleton() {
   }

   static {
      Instance$delegate = LazyKt.lazy(LazyThreadSafetyMode.SYNCHRONIZED, (Function0)null.INSTANCE);
   }

   public Singleton(DefaultConstructorMarker $constructor_marker) {
      this();
   }

   public static final class Companion {
      @NotNull
      public final Singleton getInstance() {
         Lazy var1 = Singleton.Instance$delegate;
         Singleton.Companion var2 = Singleton.Companion;
         Object var3 = null;
         return (Singleton)var1.getValue();
      }

      private Companion() {
      }

      // $FF: synthetic method
      public Companion(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }
}

在类块中初始化,使用 kotlin 的函数 LazyKt.lazy,设置 LazyThreadSafetyMode. SYNCHRONIZED 模式,LazyKt.lazy 点进去的函数为 ->

1
2
3
4
5
6
7
public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
    when (mode) {
        LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
        LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
        LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
    }

根据mode参数,下一步之行SynchronizedLazyImpl(initializer),继续进入 ->

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public actual fun <T> lazy(lock: Any?, initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer, lock)


private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
private var initializer: (() -> T)? = initializer
@Volatile private var _value: Any? = UNINITIALIZED_VALUE
// final field is required to enable safe publication of constructed instance
private val lock = lock ?: this


override val value: T
get() {
val _v1 = _value
if (_v1 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST")
return _v1 as T
}


return synchronized(lock) {
val _v2 = _value
if (_v2 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST") (_v2 as T)
} else {
val typedValue = initializer!!()
_value = typedValue
initializer = null
typedValue
}
}
}


override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE


override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."


private fun writeReplace(): Any = InitializedLazyImpl(value)
}

梳理上段代码可见,在 get 对象时,先判断_v1是否存在, !== 判断对象指向内存地址是否存在,直接判断是否是同一个对象,而不是对象值是否一致,如果对象没有内存地址,则对 lock 对象加锁进行初始化,同时 _value 声明为 Volatile 类型,保证多线程数据一致性.

补充volatile关键字原理
volatile关键字是Java中的一个类型修饰符,通过禁止编译器、CPU指令重排序和部分happens-before 规则,解决了有序性问题;通过缓存一致性协议保证每个线程获得最新值,保证每个线程拿到的参数为最新值.

静态内部类去创建单例

1
2
3
4
5
6
7
8
9
10
11
12
class Singleton private constructor(){
    companion object {
        fun getInstance()= SingletonHolder.ins
    }

    private object SingletonHolder{
        var ins = Singleton()
    }
    fun logOO():String{
        return "OO"
    }
}

枚举实现

1
2
3
4
5
6
7
8
enum class Singleton {
    INSTANCE;

    fun doSomeThing() {
        println("do some thing")
    }
}

反编译成java代码:

1
2
3
4
5
6
7
8
9
10
11
12
public final class EnumSingleton extends Enum<EnumSingleton> {
        public static final  EnumSingleton  ENUMSINGLETON;
        private static final Singleton ENUM$VALUES\[];

        public static  EnumSingleton[] values ();
        public static  EnumSingleton valueOf (String s);
        static {
            ENUM$VALUES = (new EnumSingleton[] {
                ENUMSINGLETON
            });
        };
}

通过反编译后,可以看到单例枚举类是一个final类,且创建该对象的实例是在一个static静态语句块中进行的,根据JVM的类加载机制,静态语句块只会在类被加载时执行一次,所以可以线程安全。另外因为单例枚举类反编译后实际上是一个被final修饰的类,所以他不能被继承,也就不能创建子类对象。

1、采用反射方式来创建:

1
2
3
4
5
6
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    MediaTypeEnum mp3 = MediaTypeEnum.MP3;
    // 反射方式,获取实例
    MediaTypeEnum instance = mp3.getClass().getDeclaredConstructor(String.class, int.class).newInstance(); // 但是会报错
    System.out.println(instance);
}

会报错:
unknown_filename|1888x0

unknown_filename.1|1739x0
我们在此处的报错信息打开,发现newInstance()方法直接不让enum类型利用反射来创建实例。所以利用反射方式来破坏单例模式不可能。

2、实现java.lang.Cloneable接口,调用clone方法来创建:
MediaTypeEnum mp3 = MediaTypeEnum.MP3;
// 克隆方式
Object clone = mp3.clone();
仍会报错
unknown_filename.2|1628x0

我们调用enum枚举的时候实际上调用的是父类:java.lang.Enum里面的clone()。但是Enum的clone()也是直接抛出异常,如下。所以clone()方式也是不行的。
unknown_filename.3|1787x0

3、采用实现java.io.Serializable接口,反序列化方式:
在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象,不存在创建对象的可能。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法,所以此方式也行不通。
unknown_filename.4|1492x0
综上,enum不允许通过反射创建实例,不允许通过clone()创建,也不允许通过反序列化的方式创建对象,所以可以避免反射攻击.

对比:

类型 优点 缺点
饿汉式 在类加载时就初始化,保证只进行了一次初始化,线程安全,实现简单 在类加载时就初始化,会存在资源浪费
懒汉式 方法调用时才创建对象,节省资源 线程不安全,多线程情况下存在创建多个实例的风险
懒汉式
-直接加锁
线程安全 每个线程获取对象都需要先获得锁,再执行方法,耗时
懒汉式(推荐)
-DCL
节省内存空间,并且线程安全 需要1-2次是否初始化的判断
静态内部类(推荐) 节省内存空间,并且线程安全 创建了一个新的类去调用
枚举 节省内存空间,并且线程安全,可以避免反射攻击 在类加载时就初始化,会存在资源浪费

Kotlin设计模式分享
http://peiniwan.github.io/2024/04/a84cfcb22434.html
作者
六月的雨
发布于
2024年4月6日
许可协议