单例模式 简单讲解
单例模式适合用于频繁创建的对象,重量级对象更应该采用
如果不采用单例模式,每个配置文件的内容都是一样的,
创建重复的对象 会浪费内存,
单例模式 每次只获取一个对象
减小内存开销和GC压力
package 饿汉模式;
/**
*
*/
public class HungryMode {
/**
* 所谓 饿汉式 单例,就是有一个私有的构造方法
* + 私有的静态当前类实例 + 公有的静态获取实例方法
* 由于类实例对象为 静态变量,加载类时 会创建实例对象
*
* 饿汉 故名思意,很饿,不管有多少 都要吃掉
*
* 这样会比较消耗内存
*
* 按需加载 且加载一次
*
* 优点:类加载时 完成实例化,避免线程同步问题
* 缺点:类加载时完成实例化,但是 没有达到Lazy Loading效果,如果从未使用,亏
*/
private static HungryMode sHungryMode = new HungryMode();
private HungryMode(){
System.out.println("creat" + getClass().getSimpleName());
}
public static void fun(){
System.out.println("call fun in HungryMode");
}
public static HungryMode getInstance(){
return sHungryMode;
}
public static void main(String[] args) {
HungryMode.fun();
System.out.println(HungryMode.getInstance());
}
}
package 懒汉模式;
/**
* 当 这个对象构造方法很复杂,这样的单例写法会造成 类加载很慢,会浪费很多性能
*
* 需要懒加载,所谓的懒汉式加载
*
*
* 真正需要的时候 才创建实例 不适用多线程
*/
public class LazyMode {
private static LazyMode sLazyMode;
private LazyMode(){
System.out.println("create " + getClass().getSimpleName());
}
private static LazyMode getInstance(){
if(sLazyMode == null){
sLazyMode = new LazyMode();
}
return sLazyMode;
}
public static void main(String[] args) {
System.out.println(LazyMode.getInstance());
}
}
多线程懒汉(懒汉基础上进行)优化一
package 多线程的单例;
/**
* 多线程的单例,这个单例 在 懒加载的基础上 来进行改造
*
* 多线程 会出现多个对象,需要在 获取类实例上加上““同步锁””
*
* 并且 给类实例对象加上 volatile修饰符,能保证对象的可见性
* 即在工作内存的内容更新 能立即在主内存中可见,
* 工作内存:各个线程独有的内存
* 主内存:所有线程共享的内存
* 还有一个作用:(禁止指令重排序优化)
* 有的编辑器 会进行优化,执行顺序 跟 代码顺序不同
* 这在单线程看起来可以,但是多线程不行,volatile从语义上解决了这个问题
*/
public class LazyMode {
private static volatile LazyMode sLazyMode;
private LazyMode(){
System.out.println("create " + getClass().getSimpleName());
}
//性能问题,多个线程获取实例对象 会排队等待 获取锁
//其实没必要,大多数 已经创建成功了,不用进入加锁的代码快
//进行优化, 双重校验的单例模式
public static LazyMode getInstance(){
synchronized (LazyMode.class){ //同步代码快
if(sLazyMode == null){
sLazyMode = new LazyMode();
}
}
return sLazyMode;
}
public static void main(String[] args) {
LazyMode.getInstance();
}
}
优化二:(双重校验)
package 多线程的单例;
/**
* 多线程的单例模式 采用双重校验机制
*
* 效率 和 安全 双重保证
* JDK1.5后 禁止指令重排优化
*/
public class DoubleCheckMode {
private volatile static DoubleCheckMode sDoubleCheckMode;
public DoubleCheckMode(){
System.out.println("create " + getClass().getSimpleName());
}
public static DoubleCheckMode getInstance(){
if(sDoubleCheckMode == null){
synchronized (DoubleCheckMode.class){
if(sDoubleCheckMode == null){
sDoubleCheckMode = new DoubleCheckMode();
}
}
}
return sDoubleCheckMode;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(){
@Override
public void run() {
super.run();
System.out.println("thread" + getId());
DoubleCheckMode.getInstance();
}
}.start();
}
}
}
静态内部类
package 静态内部类实例单例;
/**
* 利用静态类 只会加载一次的机制
* 使用 静态内部类 持有单例模式
*
* 保证 多线程的对象唯一性,还有 提升性能,不用同步锁机制
*/
public class InnerStaticMode {
private static class SingleTonHolder{
//静态类
public static InnerStaticMode sInnerStaticMode =new InnerStaticMode();
}
public static InnerStaticMode getInstance(){
return SingleTonHolder.sInnerStaticMode;
}
}
最推荐的一种(枚举)
package 枚举;
/**
* 利用枚举的方式 实现单例
*
* Android不推荐
*
* Effective JAVA 推荐方法
*
* 保证线程安全和单一实例的问题
*/
public enum EnumMode {
INSTANCE;
private int id;
public int getId(){
return id;
}
public void setId(int id){
this.id = id;
}
public static void main(String[] args) {
EnumMode.INSTANCE.setId(1);
System.out.println(EnumMode.INSTANCE.getId());
}
}
(1)、当我们写了 new 操作,JVM 到底会发生什么?
首先,我们要明白的是: new Singleton3() 是一个非原子操作。代码行singleton3 = new Singleton3(); 的执行过程可以形象地用如下3行伪代码来表示:
memory = allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
singleton3 = memory; //3:使singleton3指向刚分配的内存地址
但实际上,这个过程可能发生无序写入(指令重排序),也就是说上面的3行指令可能会被重排序导致先执行第3行后执行第2行,也就是说其真实执行顺序可能是下面这种:
memory = allocate(); //1:分配对象的内存空间
singleton3 = memory; //3:使singleton3指向刚分配的内存地址
ctorInstance(memory); //2:初始化对象
这段伪代码演示的情况不仅是可能的,而且是一些 JIT 编译器上真实发生的现象。
(2)、重排序情景再现
了解 new 操作是非原子的并且可能发生重排序这一事实后,我们回过头看使用 Double-Check idiom 的同步延迟加载的实现:
我们需要重新考察上述清单中的 //3 行。此行代码创建了一个 Singleton 对象并初始化变量 singleton3 来引用此对象。这行代码存在的问题是,在 Singleton 构造函数体执行之前,变量 singleton3 可能提前成为非 null 的,即赋值语句在对象实例化之前调用,此时别的线程将得到的是一个不完整(未初始化)的对象,会导致系统崩溃。下面是程序可能的一组执行步骤:
1、线程 1 进入 getSingleton3() 方法;
2、由于 singleton3 为 null,线程 1 在 //1 处进入 synchronized 块;
3、同样由于 singleton3 为 null,线程 1 直接前进到 //3 处,但在构造函数执行之前,使实例成为非 null,并且该实例是未初始化的;
4、线程 1 被线程 2 预占;
5、线程 2 检查实例是否为 null。因为实例不为 null,线程 2 得到一个不完整(未初始化)的 Singleton 对象;
6、线程 2 被线程 1 预占。
7、线程 1 通过运行 Singleton3 对象的构造函数来完成对该对象的初始化。
显然,一旦我们的程序在执行过程中发生了上述情形,就会造成灾难性的后果,而这种安全隐患正是由于指令重排序的问题所导致的。让人兴奋地是,volatile 关键字正好可以完美解决了这个问题。也就是说,我们只需使用volatile关键字修饰单例引用就可以避免上述灾难。
package 多线程的单例;
/**
* 借助 ThreadLocal 的线程安全 的懒汉式单例
*/
public class Singleton4 {
//ThreadLocal 线程局部变量
private static ThreadLocal<Singleton4> threadLocal = new ThreadLocal<>();
private static Singleton4 singleton4 = null;
private Singleton4(){}
public static Singleton4 getSingleton4(){
if(threadLocal.get() == null){ //第一词检擦:该线程是否第一次访问
createSingleton4();
}
return singleton4;
}
public static void createSingleton4(){
synchronized (Singleton4.class){
if(singleton4 == null){ //第二次检擦,是否被创建
singleton4 = new Singleton4();//只执行一次
}
}
threadLocal.set(singleton4);//将单例 放入当前线程的局部变量中
}
}