Fork me on GitHub
小单例有大秘密

小单例有大秘密

单例模式大家并不陌生,也都知道它分为什么懒汉式、饿汉式之类的。但是你对单例模式的理解足够透彻吗?今天我带大家一起来看看我眼中的单例,可能会跟你的认识有所不同。

下面是一个简单的小实例:

复制代码
//简单懒汉式
public class Singleton {
    
    //单例实例变量
    private static Singleton instance = null;
    
    //私有化的构造方法,保证外部的类不能通过构造器来实例化
    private Singleton() {}
    
    //获取单例对象实例
    public static Singleton getInstance() {
        
        if (instance == null) { 
            instance = new Singleton(); 
        }
        
        System.out.println("我是简单懒汉式单例!");
        return instance;
    }
}
复制代码

 


很容易看出,上面这段代码在多线程的情况下是不安全的,当两个线程进入if (instance == null)时,两个线程都判断instance为空,接下来就会得到两个实例了。这不是我们想要的单例。

 

接下来我们用加锁的方式来实现互斥,从而保证单例的实现。

复制代码
//同步法懒汉式
public class Singleton {
    
    //单例实例变量
    private static Singleton instance = null;
    
    //私有化的构造方法,保证外部的类不能通过构造器来实例化
    private Singleton() {}
    
    //获取单例对象实例
    public static synchronized  Singleton getInstance() {
        
        if (instance == null) { 
            instance = new Singleton(); 
        }
        
        System.out.println("我是同步法懒汉式单例!");
        return instance;
    }
}
复制代码

 

加上synchronized后确实保证了线程安全,但是这样就是最好的方法吗?很显然它不是,因为这样一来每次调用getInstance()方法是都会被加锁,而我们只需要在第一次调用getInstance()的时候加锁就可以了。这显然影响了我们程序的性能。我们继续寻找更好的方法。

 

经过分析发现,只需要保证instance = new Singleton()是线程互斥就可以保证线程安全,所以就有了下面这个版本:

复制代码
//双重锁定懒汉式
public class Singleton {
    
    //单例实例变量
    private static Singleton instance = null;
    
    //私有化的构造方法,保证外部的类不能通过构造器来实例化
    private Singleton() {}
    
    //获取单例对象实例
    public static Singleton getInstance() {
        if (instance == null) { 
            synchronized (Singleton.class) {
                if (instance == null) { 
                    instance = new Singleton(); 
                }
            }
        }
        System.out.println("我是双重锁定懒汉式单例!");
        return instance;
    }
}
复制代码

 

这次看起来既解决了线程安全问题,又不至于每次调用getInstance()都会加锁导致降低性能。看起来是一个完美的解决方案,事实上是这样的吗?

很遗憾,事实并非我们想的那么完美。java平台内存模型中有一个叫“无序写”(out-of-order writes)的机制。正是这个机制导致了双重检查加锁方法的失效。这个问题的关键在上面代码上的第5行:instance = new Singleton(); 这行其实做了两个事情:1、调用构造方法,创建了一个实例。2、把这个实例赋值给instance这个实例变量。可问题就是,这两步jvm是不保证顺序的。也就是说。可能在调用构造方法之前,instance已经被设置为非空了。下面我们一起来分析一下:

 

假设有两个线程A、B

1、线程A进入getInstance()方法。

2、因为此时instance为空,所以线程A进入synchronized块。

3、线程A执行 instance = new Singleton(); 把实例变量instance设置成了非空。(注意,是在调用构造方法之前。)

4、线程A退出,线程B进入。

5、线程B检查instance是否为空,此时不为空(第三步的时候被线程A设置成了非空)。线程B返回instance的引用。(问题出现了,这时instance的引用并不是Singleton的实例,因为没有调用构造方法。) 

6、线程B退出,线程A进入。

7、线程A继续调用构造方法,完成instance的初始化,再返回。 

 

难道就没有一个好方法了吗?好的方法肯定是有的,我们继续探索!

复制代码
//解决无序写问题懒汉式
public class Singleton {
    
    //单例实例变量
    private static Singleton instance = null;
    
    //私有化的构造方法,保证外部的类不能通过构造器来实例化
    private Singleton() {}
    
    //获取单例对象实例
    public static Singleton getInstance() {
        if (instance == null) { 
            synchronized (Singleton.class) {                  //1
                Singleton temp = instance;                //2
                if (temp == null) {
                    synchronized (Singleton.class) {  //3 
                        temp = new Singleton();   //4    
                    }
                    instance = temp;                  //5      
                }
            }
        }
        System.out.println("我是解决无序写懒汉式单例!");
        return instance;
    }    
}
复制代码

 


1、线程A进入getInstance()方法。

2、因为instance是空的 ,所以线程A进入位置//1的第一个synchronized块。

3、线程A执行位置//2的代码,把instance赋值给本地变量temp。instance为空,所以temp也为空。 

4、因为temp为空,所以线程A进入位置//3的第二个synchronized块。

5、线程A执行位置//4的代码,把temp设置成非空,但还没有调用构造方法!(“无序写”问题) 

6、线程A阻塞,线程B进入getInstance()方法。

7、因为instance为空,所以线程B试图进入第一个synchronized块。但由于线程A已经在里面了。所以无法进入。线程B阻塞。

8、线程A激活,继续执行位置//4的代码。调用构造方法。生成实例。

9、将temp的实例引用赋值给instance。退出两个synchronized块。返回实例。

10、线程B激活,进入第一个synchronized块。

11、线程B执行位置//2的代码,把instance实例赋值给temp本地变量。

12、线程B判断本地变量temp不为空,所以跳过if块。返回instance实例。

 

到此为止,上面的问题我们是解决了,但是我们突然发现为了解决线程安全问题,但给人的感觉就像身上缠了很多毛线.... 乱糟糟的,所以我们要精简一下:

复制代码
//饿汉式
public class Singleton {
    
    //单例变量 ,static的,在类加载时进行初始化一次,保证线程安全 
    private static Singleton instance = new Singleton();    
    
    //私有化的构造方法,保证外部的类不能通过构造器来实例化。     
    private Singleton() {}
    
    //获取单例对象实例     
    public static Singleton getInstance() {
        System.out.println("我是饿汉式单例!");
        return instance;
    }
}
复制代码

 

看到上面的代码,瞬间觉得这个世界清静了。不过这种方式采用的是饿汉式的方法,就是预先声明Singleton对象,这样带来的一个缺点就是:如果构造的单例很大,构造完又迟迟不使用,会导致资源浪费。

 

到底有没有完美的方法呢?继续看:

复制代码
//内部类实现懒汉式
public class Singleton {
    
    private static class SingletonHolder{
        //单例变量  
        private static Singleton instance = new Singleton();
    }
    
    //私有化的构造方法,保证外部的类不能通过构造器来实例化。
    private Singleton() {
        
    }
    
    //获取单例对象实例
    public static Singleton getInstance() {
        System.out.println("我是内部类单例!");
        return SingletonHolder.instance;
    }
}
复制代码

 

懒汉式(避免上面的资源浪费)、线程安全、代码简单。因为java机制规定,内部类SingletonHolder只有在getInstance()方法第一次调用的时候才会被加载(实现了lazy),而且其加载过程是线程安全的(实现线程安全)。内部类加载的时候实例化一次instance。

 

简单说一下上面提到的无序写,这是jvm的特性,比如声明两个变量,String a; String b; jvm可能先加载a也可能先加载b。同理,instance = new Singleton();可能在调用Singleton的构造函数之前就把instance置成了非空。这是很多人会有疑问,说还没有实例化出Singleton的一个对象,那么instance怎么就变成非空了呢?它的值现在是什么呢?想了解这个问题就要明白instance = new Singleton();这句话是怎么执行的,下面用一段伪代码向大家解释一下:

mem = allocate();             //为Singleton对象分配内存。
instance = mem;               //注意现在instance是非空的,但是还没有被初始化。

ctorSingleton(instance);    //调用Singleton的构造函数,传递instance.

 

由此可见当一个线程执行到instance = mem; 时instance已为非空,如果此时另一个线程进入程序判断instance为非空,那么直接就跳转到return instance;而此时Singleton的构造方法还未调用instance,现在的值为allocate();返回的内存对象。所以第二个线程得到的不是Singleton的一个对象,而是一个内存对象。

 

以上就是就是我对单例模式的一点小小的思考跟理解,热烈欢迎各位大神前来指导批评。

 

 

 

 
分类: 架构设计设计模式

变量内存分配知多少

 

  繁忙的工作总容易让我们忽视最基础的知识,手里的活停一停,下楼呼吸下新鲜空气(北京的朋友抱歉了),让大脑切换下进程。

  回想工作中我们所遇到的难点,嗯,好多都是我们对基础知识了解得不够透彻,或者说只知道了表层的东西。而往往我们总是被这些表层东西所欺骗了,最后等待我们的就是bug量增多,性能低下,运行不稳定,维护成本剧增,客户满意度下降,更严重的便可能导致项目进入恶性循环。可想而知,我们所面临的挑战是多么艰巨。

  言归正传,本节我们主要讲的内容是.Net中变量内存分配,是的,就讲这么简单的知识。

一、我们先看下类的成员变量:

public class Customer

{

        int customerId;

        string customerName;

}

 

public class Customer

{

        int customerId = 0;

        string customerName = null;

}

  乍看之下,我们会觉得第二种写法更加合情合理,但我们的编译器没有那么智能。他总会首先帮我们将成员变量赋予一个初始值,当我们人为赋予初始值的时候,编译器会在调用默认构造函数之前用我们赋予的初始化值替换他默认初始化的值。也就是除非我们初始化的数据是业务相关数据,否则我们赋默认值则给JIT增加了负担。

二、静态变量:

  当我们增加个CustomerCount int类型静态变量,并赋予初始值0时,这个时候我们会发现,编译器默认给我们创建了一个静态构造函数,并在静态构造函数中替换掉编译器默认初始化的值。如果我们不人为初始化呢,其实编译器还会自动为我们提供一个静态的构造函数,除非是我们的类没有静态变量。静态构造函数只会执行一次

三、常量:

  1、我们知道静态常量无需分配内存的,而且必须定义的时候就得赋初始化值,这样的优点我们也不难发现他会带来潜伏的bug,当他的引用(其实是那个常量值)分布到不同的程序集中时,如果后期由于业务变化需要改变常量值时,项目必须整体重新编译一次,否则其他引用的程序集常量还保持原来值。可以看到,这给我们维护带来了不便。

  2、由于静态常量带来的不便,所以这个时候动态常量便应运而生了,动态常量需要分配内存的,其必须在声明或在构造函数中赋初始化值,如果不人为赋值,编译器会初始化默认值的,但这样的常量是毫无意义的。

四、局部变量:

  我们都很清楚,C#中访问局部变量的之前,我们必须人为为他赋初值,如果没有赋值,则编译就不会通过。其实这个时候,编译器是很聪明的,我们声明了一个局部变量,但不给他赋值,我们要这个局部变量干嘛呢(闭包情况不在这个范围)?他并不是类成员变量,可以共享。反过来想一想,如果编译器也为局部变量赋初值,那么JIT的负担得多么重。所以默认情况下,C#是不会初始化局部变量的。

下面我们来看看局部变量内存分配的情况:

public class Customer

{

        public Customer GetCustomer()

        {

            int customerId = 4;

            Customer customer = GetCustomerById(customerId);

            return customer;

        }

 

        private Customer GetCustomerById(int id)

        {

            Customer customer = new Customer();

            return customer;

        }

}

好,具体情况我们来看图:

说明:①②给局部变量customerId赋值。

③④将this和customerId值压入堆栈,准备调用GetCustomerById方法。

说明:⑤⑥调用GetCustomerById方法

⑦⑧在托管堆上创建Customer对象,并给局部变量customer赋值

说明:⑨⑩给返回值returnObj赋值,returnObj是编译器创建的临时存放返回值得局部变量

⑪将返回值的内容压入堆栈

说明:⑫GetCustomerById方法执行完毕,将返回值传递到调用方维护的堆栈里。此时GetCustomerById局部变量都被堆栈弹出,这部分内存自动收回。

⑬给局部变量customer赋值

说明:⑭⑮给局部变量returnObj赋值

⑯将返回值压入堆栈中

  上图,只是为了形象说明局部变量内存分配情况,并不是最终本地代码执行时内存情况,JIT还会为我们做很多优化,而且局部变量表没有体现到堆栈中。

 

  从上图中,我们可以知道,局部变量中值类型的值存放到堆栈中(并不是所有值类型都存放在堆栈区,有例外情况,比如闭包和yield的情况就比较特殊,编译器会提升局部变量的),而引用类型则是在托管堆中开辟内存存放引用类型对象,其对象引用则是存放到堆栈中。

    ——Aaron.Pan

 
 
分类: .Net微观世界

(2)学习IOS,没想象中的神秘

2013-06-16 20:41 by whitejade, 153 阅读, 2 评论, 收藏编辑

这两天我开始培训了。之前第一篇文章有人说很像软文,哈哈,我推广啥呢?培训机构?反正是北京,而北京就那么几家比较大的培训机构。在这记录一下这几天的学习进度吧。我培训的地方会免费放视频到网上,想要的可以找找“itcast” +“ios”关键字。还是书籍?说实话,学习IOS书还真挺少,也不要我来推广。

自学部分:

这几天快把书给自学完了。整体感觉这本书《objective-c程序设计 第四版》还不是很细致,有些翻译不够到位,而且一些知识旧了也没有及时删除。对于初学者来说,可能根本不懂“发送消息”和“调用方法”是同一回事。在看书的过程中,还是要仔细认真的,因为书是比较凝炼的。有时候看完了一句话,不一定能够理解。所以有自己的总结过程是非常重要的。我就在旁边处写了很多自己的总结。最关键的还是不懂就要问,每天针对自己不懂的,去咨询牛人,这样疑惑很快就解决了。不会有很深的挫败感。我每天还是认真做作业了,如果谁有需要,我可以把我做的练习答案发到这来交流。

 

培训:

因为刚开始2天,所以也没有接触到很深的知识。老师主要首先介绍了一下ios和mac的基本知识,再就是从c讲起。今天就做了自己的第一个c程序。在这两天中,感触最深刻的自我介绍环节是:同学各种背景,来自不同的地方,有着不一的学历,有硕士,有中专生,有零基础,也有几年工作经验的开发者,有从教师转行的,从美工转行的,从硬件转行的,从创业经理来着,年龄从30跨越到17岁……当然还是男的多,女的少,此外,大家学习ios的目的也不一,有的想做外包,有的想做游戏创业,有的纯粹为了IOS的前途,或者纯粹就是想在北京混下去……,但是大家都聚在一起,这种感觉只能自己来体会了。坚持到底,看看自己能否成功改变自己的人生轨迹?我很期待很期待!

规划:

这两天主要是听课,所以代码就敲得少了。我在此还是要明确目标:我参加培训至少要找个好工作,尤其是要结合我的优势:英语!虽然有人说IOS程序员现在貌似有饱和趋势,也是,中国人多,做啥事都是跟风,一跟风就人多了。但我认为:关键还是要学好。所谓,追求卓越,成功会在不经意间追上你。这个就是我希望自己能够达到的。在这几个月之内,首先要打好ios基础,有空练好口语,想好创意,多写代码。最重要的是,早点开始做项目,等面试的时候能够拿出去做筹码的。

 

 

 

 
 
 
标签: IOS 培训
posted on 2013-06-17 12:48  HackerVirus  阅读(187)  评论(0编辑  收藏  举报