java多线程编程(二)----单例模式

一.java中的设计模式

单例模式是一种设计模式,就比如下棋的时候对于高手来说,每个人都会很多种棋谱,在比赛中按照棋谱的套路灵活应用,见招拆招。java中的设计模式就和棋谱一样,程序员按照棋谱来写代码能够保证下限。设计模式有很多种,之前有个大佬写了一本书有23种设计模式,不同的语言有不同的设计模式。对于新手来说最主要的是理解2种设计模式,单例模式和工产模式。

二.单例模式

单例模式是一种设计模式,用于确保一个类只能有一个实例,也就是只能new一次,并提供全局访问这个实例的方式。单例模式的基本思想是将类的实例化过程封装起来,使得整个程序中只有一个对象能够被创建。这个唯一的对象被称为单例对象,他可以被类的所有方法共享。
单例模式主要分为2种饿汉模式和懒汉模式。

一.饿汉模式

饿汉模式是在类加载的时候创建出实例

class Single{
    private static Single single = new Single();
    public static Single create(){
       return single;
    }
    private Single(){

    }
}
public class Test12 {
    public static void main(String[] args) {
         Single str1=Single.create();
         Single str2 = Single.create();
        System.out.println(str1==str2);
    }
}

在这里插入图片描述
在这里插入图片描述
Singlel类带有static,类属性,由于每个类对象是单例的,类对象的属性(static),也就是单例的。代码的执行时机是在Single类被jvm加载的时候,Single类会在jvm第一次使用的时候加载。如果我们继续new对象会发生什么?
在这里插入图片描述
我们发现new对象的时候编译器报错,这是应为Single类的构造方法是私有的,出了该类则不能访问。那么一定不能访问吗?
尽管该类的构造方法是私有的,但是可以使用反射去访问,创造出多个实例。反射是属于非常规的编程手段,正常开发的时候,不应该或者慎用反射。滥用反射,会带来极大的风险,会让代码变的抽象难以维护。java也有其他方式实现单例模式不怕反射。

二.懒汉模式(非线程安全)

懒汉模式是第一次使用实例的时候就创建,能不创建就不创建。

class Single{
    private static Single single = null;
    public static Single create(){
     if(single==null){
        single = new Single();
     }
     return single;
    }
    private Single(){

    }
}
public class Test12 {
    public static void main(String[] args) {
         Single str1=Single.create();
         Single str2 = Single.create();
        System.out.println(str1==str2);


    }
}

三.线程安全下的懒汉模式

对比一下饿汉模式和懒汉模式思考一下谁是线程安全的,谁是不安全的。
在这里插入图片描述
先看饿汉模式多个线程读取single的值,上篇我们讲线程安全的时候谈过,造成线程安全问题,其中一点是多个线程同时修改同一变量,而我们饿汉模式只是读并没有修改,所以饿汉模式是线程安全的。

接着看懒汉模式
在这里插入图片描述
懒汉模式中涉及3种操作,读取,if(比较),创建实例。读取操作是线程安全的,if(比较)不是原子性的,线程安全问题中也有一点不是原子性的操作也会造成线程安全问题。其原理是:
在这里插入图片描述
当还没有创建实例时,2个线程都在调用create方法,通过if语句去判断是否已经创建出实例,由于并没有创建,2个线程都进入if语句,创建2次实例。那么你会有此疑问,创建2次实例就创建2次,虽然创建了2次实例,但是第二次创建的引用给single本质还是只有一个实例。这种想法是正确的,但是创建实例在代码量很多的时候效率很低,所以我们应该避免创建2次实例,给代码加锁就能有效避免这个问题。

class Single{
    private static Single single = null;
   
    public static Single create(){
       synchronized (Single.class) {
           if (single == null) {
               single = new Single();
           }
       }
     return single;
    }

    private Single(){

    }
}
public class Test12 {
    public static void main(String[] args) {
         Single str1=Single.create();
         Single str2 = Single.create();
        System.out.println(str1==str2);
        
    }
}

上述虽然给代码加了锁避免了多个线程访问的时候创建出多个对象,但这种情况在多线程访问的时候即使已经创建出实例但每次判断的时候都要给代码加锁,加锁本来是一件效率很低的事情,我们应该避免无脑加锁,那么怎么让没有创建出实例的时候加锁,后面每次判断的时候不加锁呢?

class Single{
    private static Single single = null;

    public static Single create(){
        if(single==null) {
            synchronized (Single.class) {
                if (single == null) {
                    single = new Single();
                }
            }
        }
     return single;
    }

    private Single(){

    }
}
public class Test12 {
    public static void main(String[] args) {
         Single str1=Single.create();
         Single str2 = Single.create();
        System.out.println(str1==str2);
        
    }
}

多加一个if条件判断是否要加锁然后加锁。第一次创建实例的时候,假设2个线程走到第一个if语句,线程1先拿到锁进入第二个if语句创建出实例,解锁后第二个线程拿到锁,进入第二个if语句发现不满足条件不创建实例,当后续代码进入第一个if语句该实例已经不为空了,直接返回,所以不会在继续加锁。保证了只创建出一个实例,同时也在第一次的时候加了一次锁。

四.懒汉模式下的内存可见性和指令重排性

在这里插入图片描述
虽然防止了重复加锁,但加锁操作可能阻塞,当第一次加锁时,第二个if条件和第一个if条件可能会间隔非常多的时间,在这个很长的时间间隔中,可能别的线程就把single的值给改了,single获取不到新修改的值导致会出现错判,为了防止这个操作加上volatile,让线程时刻获取着single的值。

class Single{
    private volatile static Single single = null;

    public static Single create(){
        if(single==null) {
            synchronized (Single.class) {
                if (single == null) {
                    single = new Single();
                }
            }
        }
     return single;
    }

    private Single(){

    }
}
public class Test12 {
    public static void main(String[] args) {
         Single str1=Single.create();
         Single str2 = Single.create();
        System.out.println(str1==str2);

    }
}

加上voliatle还有一个作用防止指令重排性。

五.指令重排性

指令重排性也是编译器的一种优化手段,保持原有的逻辑不变的情况下,对代码执行顺序进行调整,使调整后的执行效率更高。比如超市买菜,我想买西红柿,萝卜,黄瓜,菜花。
在这里插入图片描述
如果我按照自己想买的顺序走那么会绕一大圈,如果我按照此顺序:西红柿,黄瓜,菜花,萝卜这种顺序走效率会提高,编译器也是这样在原有的逻辑不变的情况下对顺序进行调整。在这里插入图片描述
这个代码:1.给对象创建出内存空间,得到内存地址
2.在空间上调用构造方法,对对象进行初始化
3.把内存地址付给single引用。
正常顺序是1,2,3。但是编译器优化后可能1,3,2并且执行3之后,2之前出现了线程切换,此时还没来得及给对象初始化,就调用给别的线程了,如果给single加上voliatle之后就不会出现指令重排性了。

  • 24
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值