注册

你真的会用单例么?

单例是什么?

是一种对象创建模式,可以确保项目中一个类只产生一个实例。

好处

对于频繁使用的对象可以减少创建对象所花费的时间,这对于重量级对象来说,简直是福音。由于new的减少,对系统内存使用频率也会降低,减少GC的压力,并缩短GC停顿时间,这也会减少Android项目的UI卡顿。

如何实现单例

1、饿汉模式

public class TestSingleton {

private static final TestSingleton testSingleton = new TestSingleton();

private TestSingleton(){

}

public static TestSingleton getInstance(){
return testSingleton;
}

}

细节我就不多写了,大家都应该知道,构造函数为private,用getInstance来获取实例

2.、懒汉模式

public class TestSingleton {

private static TestSingleton testSingleton;

private TestSingleton(){

}

public static TestSingleton getInstance(){
if(testSingleton==null){
testSingleton = new TestSingleton();
}
return testSingleton;
}

}

比饿汉式的优点在于用时再加载,比较重量级的单例,就不适用与饿汉了。

3、线程安全的懒汉模式

public class TestSingleton {

private static TestSingleton testSingleton;

private TestSingleton(){

}

public static TestSingleton getInstance(){
if(testSingleton==null){
synchronized (TestSingleton.class){
testSingleton = new TestSingleton();
}
}
return testSingleton;
}

}

可以看到的是比上面的单例多了一个对象锁,着可以保证在创建对象的时候,只有一个线程能够创建对象。

4、线程安全的懒汉模式-DCL双重检查锁机制

public class TestSingleton {

private static volatile TestSingleton testSingleton;

private TestSingleton(){

}

public static TestSingleton getInstance(){
if(testSingleton==null){
synchronized (TestSingleton.class){
if(testSingleton==null){
testSingleton = new TestSingleton();
}
}
}
return testSingleton;
}

}

双重检查,同步块加锁机制,保证你的单例能够在加锁后的代码里判断空,还有增加了一个volatile 关键字,保证你的线程在执行指令时候按顺序执行。这也是市面上见的最多的单例。

敲黑板!!知识点:原子操作、指令重排。

什么是原子操作?

简单来说,原子操作(atomic)就是不可分割的操作,在计算机中,就是指不会因为线程调度被打断的操作。

m = 6; // 这是个原子操作

假如m原先的值为0,那么对于这个操作,要么执行成功m变成了6,要么是没执行m还是0,而不会出现诸如m=3这种中间态——即使是在并发的线程中。

而,声明并赋值就不是一个原子操作:

int n = 6; // 这不是一个原子操作

对于这个语句,至少有两个操作:

  1. 声明一个变量n
  2. 给n赋值为6

这样就会有一个中间状态:变量n已经被声明了但是还没有被赋值的状态。

在多线程中,由于线程执行顺序的不确定性,如果两个线程都使用m,就可能会导致不稳定的结果出现。

什么是指令重排?

简单来说,就是计算机为了提高执行效率,会做的一些优化,在不影响最终结果的情况下,可能会对一些语句的执行顺序进行调整。

int a ;   // 语句1 

a = 8 ; // 语句2

int b = 9 ; // 语句3

int c = a + b ; // 语句4

正常来说,对于顺序结构,执行的顺序是自上到下,也即1234。

但是,由于指令重排的原因,因为不影响最终的结果,所以,实际执行的顺序可能会变成3124或者1324。

由于语句3和4没有原子性的问题,语句3和语句4也可能会拆分成原子操作,再重排。

也就是说,对于非原子性的操作,在不影响最终结果的情况下,其拆分成的原子操作可能会被重新排列执行顺序。

主要在于testSingleton = new TestSingleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。

  1. 给 testSingleton 分配内存
  2. 调用 testSingleton 的构造函数来初始化成员变量,形成实例
  3. 将testSingleton 对象指向分配的内存空间(执行完这步 testSingleton 才是非 null 了)

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 testSingleton 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

--------------------------------------------一部分的文章可能讲到如上就嘎然而止了----------------------------------------

推荐后两种

5、静态内部类来实现单例

public class TestSingleton {

private TestSingleton(){

}

public static TestSingleton getInstance(){
return TestSingletonInner.testSingleton;
}

private static class TestSingletonInner{
static final TestSingleton testSingleton = new TestSingleton();
}

}

static 保证数据独一份

final 初始化完成后不能被修改,线程安全。

敲黑板!!知识点:java在加载类的时候不会将其内部的静态内部类加载,只有在使用该内部类方法时才被调用。这明显是最好的单例,并不需要什么锁一类的机制。

利用了类中静态变量的唯一性

优点:

  1. jvm本身机制保证线程安全。
  2. synchronized 会导致性能问题。
  3. TestSingletonInner 是私有的,除了通过TestSingleton 访问,没有其他访问的可能性。

6、枚举单例

public enum  TestSingleton {

INSTANCE;

public void toSave(){
}

}

使用TestSingleton.INSTANCE.toSave();

创建枚举实例的过程是线程安全的,所以这种写法也没有同步的问题。如果你要自己添加一些线程安全的方法,记得控制线程安全哦。

优点:写法简单/线程安全

Android中的单例实际应用

1、application

本身就是单例,生命周期为整个程序的生命周期,可以通过这个特性,能够用来存储一些数据

2、单例模式引起的内存泄漏

在使用Context注意用application中的context

0 个评论

要回复文章请先登录注册