您的位置:首页 > 编程语言 > Java开发

java单例设计模式详解(懒汉饿汉式)+深入分析为什么懒汉式是线程不安全的+解决办法

2018-12-04 23:27 429 查看

对于java单例设计模式,一直想写一篇博客,但是关于java单例设计模式涉及的知识比较多,后面可能还要牵涉多线程问题,有一些知识我自己也一直没明白,就一直放呢没有写,现在我觉得大部分关于单例设计的知识包括多线程的知识我也明白了,于是今天就写出来分享给大家,如果文章里边有不恰当的地方还请大家留言指出,感激不尽。

单例设计模式就是:运用单例模式设计的这个类在每次实例化的时候只能产生一个对象。比如A类是利用单例设计模式设计的一个类,现在B,C两个类都需要使用到A类的实例怎么办?这个时候就要看谁先实例化的A类了,如果是B类第一次实例化的A类,那么A类就只能实例化这一次了,C类调用A类的时候检查到A类已经被实例化过了,那么他就只能使用已经实例化过的A类。是不是A,B,C绕来绕去把你们绕迷了。那么下面就进入代码实战。

还有一点需要注意,一个类如果只能实例化一次,那么它的构造函数一定是私有的。如果它的构造是public的话,那么需要使用到该类的所有类都能实例化这个类了,那就不行了。所以这个类必须是私有的。既然是私有的那就只能自己调用自己来实例化了也就顺理成章的想到通过一个方法return new A();返回一个对象。

下面这几行代码就是一个最简单的单例设计模式,下面我们就从这个最简单的开始一步一步深入讲解。

[code]public class Singleton {
private Singleton(){}

public Singleton getInsatnce(){
return new Singleton();
}
}

以上代码虽然完成了我们上面的要求,但是getInstance()这个方法不能直接调用啊如果不实例化Singletonle这个类的话。那么自然就想到把这个类static化,使用static关键字就可以在不产生实例化对象的情况下去调用该方法。于是就变成:

[code]public class Singleton {

private Singleton(){}

public static Singleton getInsatnce(){
return new Singleton();
}
}

但是这样的话有一个问题就是每次调用getInstance()方法的时候都会new一个Singleton对象,所以为了只让它产生一个实例化对象,我们提前造一个对象,在该方法中返回就行了。

[code]public class Singleton {

private static Singleton singleton=new Singleton();//这里为什么要使用private 和static两个关键字修饰是有原因的,希望你们能弄清楚。
private Singleton(){}

public static Singleton getInsatnce(){
return singleton;
}
}

以上我们便是实现了著名的饿汉式单例,是不是看起来很饿,没人叫他他就先实例化好自己,整装待发时刻准备着去吃饭。也正是有这一个特性,使用饿汉式有一点不好就是浪费资源,只要这个方法一加载立即就产生对象,产生对象那是要占用堆内存和栈内存的啊。你说这是浪费资源吗。所以我们来看懒汉式。懒汉式单例与饿汉式刚好相反,别人不叫他他就不实例化自己刚好解决了上面出现的问题。但是他也有缺点,下面分析缺点,这里先看代码:

[code]public class Singleton {

private static Singleton singleton; //-----------1

private Singleton(){//;-----------2
//;-----------3
}//;-----------4

public static Singleton getInsatnce(){//;-----------5
if (singleton == null){//;-----------6
singleton = new Singleton();//;-----------7
}//;-----------8
return singleton;//;-----------9
}
}

这就是大名鼎鼎的懒汉式单例设计模式,只有当你要使用这个类的时候才加载这个类,其他时候不加载。但是懒汉式设计虽然好用,但是它是线程不安全的。

什么是线程不安全?那就接着往下看把!

为了测试上面的类在被实例化的时候到底产生类几个对象,我们就在私有的构造方法中输出一行话,因为每产生一个新的类都要调用构造的,意思就是产生几个类构造中的这句话就被输出几次,下面我们来实现这个思路:

[code]class Singleton {

private static Singleton singleton; //-----------1

private Singleton(){//;-----------2
System.out.println("这是在构造方法中的一句话,用来验证产生了几个对象");//;-----------3
}//;-----------4

public static Singleton getInsatnce(){//;-----------5
if (singleton == null){//;-----------6
singleton = new Singleton();//;-----------7
}//;-----------8
return singleton;//;-----------9
}
}
public class TestDemo{
public static void main(String args[]) {
new Thread(new Runnable() {
public void run() {
Singleton single=Singleton.getInsatnce();
}
},"线程A").start();
new Thread(new Runnable() {
public void run() {
Singleton single=Singleton.getInsatnce();
}
},"线程B").start();
}
}

输出结果:

[code]这是在构造方法中的一句话,用来验证产生了几个对象
这是在构造方法中的一句话,用来验证产生了几个对象

或者输出:

这是在构造方法中的一句话,用来验证产生了几个对象(多运行几次就出现了)

纳尼?为什么会出现两种结果?不是说好的吗只会输出一句话的吗,为什么还会输出两句话?我也在程序中的第6句话判断了啊?怎么还会输出两句话?别着急,容我慢慢分析。如果正常的话还谈什么线程不安全,输出错误才是正常的。

之所以出现错误是因为线程A和线程B的执行是随机的(随机就是不知道cpu什么是时候切换,反正1每个线程执行的总时间差不多是一样的),两个线程到底谁先执行,执行到那句代码停止让出资源让另外的一个线程执行,这也是随机的,具体的分配是靠进程去处理。所以,我们可以这样去分析:

假如当线程A先去执行,执行到第6句话的时候(第六句话执行完了),恰好停止了,这时候线程B就开始执行,线程B执行完第7句话才停止,释放资源让线程A去执行,但是A已经判断过了啊,已经执行完第六句话了啊,于是A继续执行第7句话,于是再次new了一个Singleton对象,因此,虽然已经判断了但是还是造了两个对象出来,到此分析完毕!

我们明白了问题的产生原理,那么怎么解决这个问题呢?那只有使用神奇的synchronized关键字了。那么怎么使用这个关键字来解决问题?明天继续分析,睡觉去了。。。。

接着昨天的分析:

昨天晚上已经分析完了为什么会产生,因为昨晚太累了,就没接着再写下去,今天我们继续昨天的话题,既然问题出现了,那么我们应该怎么使用synchronized关键字化解这场危机呢?

关于synchronized关键字有两种使用方法,一种是同步代码块(不知道这两种方法的可以自行百度),另一种是同步方法,至于使用哪一种,根据自己代码中的场景。下面我们使用同步方法把synchronized关键字加在getInstance()这个方法前面。代码如下:

[code]class Apple{
private static Apple apple=null;
private Apple() {
System.out.println("这是在苹果的构造方法中,主要是用来验证Apple类被实例化了几次");
}
public synchronized static Apple getInstance () {
if(apple==null) {
apple=new Apple();
}
return apple;
}
}
public class TestDemo{
public static void main(String args[]) {
new Thread(new Runnable() {
public void run() {
Apple apple=Apple.getInstance();
}
},"线程A").start();
new Thread(new Runnable() {
public void run() {
Apple apple=Apple.getInstance();
}
},"线程B").start();
}
}

看见没有,把synchronized关键字加在了getInstance()方法的前面,问题肯定会得以解决,请看下面输出:

[code]这是在苹果的构造方法中,主要是用来验证Apple类被实例化了几次

就算多运行几次还是只输出一个,意思是只实例化了这一次。但是对然问题解决了。有一点不完美的地方就是把整个方法都加上了synchronized关键字,无疑会使得代码等待的时间变长,一次也就拖慢了代码的执行效率。那我们能不能不把整个方法加锁,只把可能出现问题的地方加一把锁可以吗?当然可以了,下面请看代码?

[code]class Apple{
private static Apple apple=null;
private Apple() {
System.out.println("这是在苹果的构造方法中,主要是用来验证Apple类被实例化了几次");
}
public static Apple getInstance () {
if(apple==null) {//-----------------------1
synchronized(Apple.class) {//---------2
if(apple==null) {//---------------3
apple=new Apple();
}
}
}
return apple;
}
}
public class TestDemo{
public static void main(String args[]) {
new Thread(new Runnable() {
public void run() {
Apple apple=Apple.getInstance();
}
},"线程A").start();
new Thread(new Runnable() {
public void run() {
Apple apple=Apple.getInstance();
}
},"线程B").start();
}
}

输出:

[code]这是在苹果的构造方法中,主要是用来验证Apple类被实例化了几次

这次使用的是同步代码块的方式来解决的,至于同步代码块的方式怎么使用的,为什么要在synchronized()里面加上Apple.class,这些问题都是关于同步代码块怎么使用的,在这里不做详细解释。

我们主要关注点是注释的1,2,3这几行代码,这几行代码也就是大名鼎鼎的双重锁机制,你可能会疑惑了,为什么1那里已经判断一次apple是否为空了,怎么在3那里还要再判断一次?岂不是多余吗?这个问题我一开始遇到的时候也苦思半天,再我生气的把3那里的判断去掉之后发现实例化了两次,很明显3那里的判断是不能去掉的,至于为什么不能去掉,大家可以自行实验,自己多想想,我不想过多的解释了,自己想通了才是自己真正懂了。(提醒大家一下,问题还是出在1到2转换的时候)

现在我们的双重检验锁模式既解决了在多线程中重复创建对象问题,又提高了代码执行效率,同时还是懒加载模式,是不是已经非常完美了,明确告诉你,还没有,什么????还没有?why?为什么?这就要涉及到java虚拟机的知识了。

我们知道对象在创建的过程中执行了以下步骤:1,开辟堆内存和栈内存,把变量命名称存在栈内存,堆内存中暂时只开辟一块空间,因为还没有实例化里面的所以为空。2,当实例化了对象之后即new了之后,占内存中的变量就指向堆内存的区域,也就是堆内存空间的首地址(比如是0x00001)给了栈内存中的变量,在这里是apple。这是对象创建过程的基本步骤,但是java语言为了提高代码执行效率,有时候并不会按照常规出牌,可能故意打乱代码执行顺序,为了不让代码执行顺序被打乱,可以使用

volatile关键字来修饰
。至于怎么使用
volatile关键字,自行百度,我自己也不是很熟。好了本次的内容就讲到这里。以后正真在开发使用到了单例设计模式的安全写法,我再详细补充。

 

 

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐