设计模式之单例模式

java中单例模式是一种常见的设计模式,单例模式的写法有好几种,这里主要介绍两种:懒汉式单例、饿汉式单例

单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。

饿汉式单例

//饿汉式
public class Singleton {
     private static Singleton singleton= new Singleton(); //自动的创建一个唯一的实例
     private Singleton(){   //构造私有,外界无法直接创建实例
     
	 } 
     public static Singleton  getInstance(){  //提供获得的唯一途径
         return singleton;
     }
}

懒汉式


//懒汉式
public class Singleton {
     private static Singleton singleton;
     private Singleton(){}
     public static Singleton  getInstance(){//提供获得的唯一途径
         if (singleton==null){
             return new Singleton();
         }
         return singleton;
     }
}

饿汉式是线程安全的,但懒汉式其实是有问题的,不知道大家发现没有

在多线程的情况下,懒汉式是可以创建多个实例的,比如有两个线程A,B,当A线程进入到懒汉式的

if (singleton==null){ //刚刚执行完
      return new Singleton();
}

此时cpu把执行权交给了B线程,B也执行到这里

if (singleton==null){ //刚刚执行完
      return new Singleton();
}

这是A,B线程同时执行,就会产生两个实例对象,可以想象,在高并发的情况下,可以产生多少个这样的实例,那怎么才安全呢?
加锁,像下面这样

public class LazeSingleton {
    private LazeSingleton(){}
    private static LazeSingleton lazeSingleton;
    public static synchronized LazeSingleton getInstance(){//同步方法
        if(lazeSingleton==null){
            lazeSingleton = new LazeSingleton();
        }
        return lazeSingleton;
    }
}

这样倒是安全了,但是我只要第一次获取的时候才实例化,后面获取直接拿就是,你给我加锁,这效率是不是有点低啊,那改成这样呢

class LazzySingleTon{
    private static LazzySingleTon singleTon;
    private LazzySingleTon(){}

    public  static LazzySingleTon getSingleTon(){
            if (singleTon == null){
                synchronized (LazzySingleTon.class){ 
                    if (singleTon == null){
                        singleTon = new LazzySingleTon();
                    }
                }
            }
            return singleTon;
    }
}

这样倒是可以保证获取到的是同一个对象,但是就真的安全吗
答案明显不是,可能会由于指令重排造成空指针异常
原因如下:

  singleTon = new LazzySingleTon();不是原子性操作,
  				会有如下操作:
                    1.分配内存空间,
                    2.执行构造方法,初始化对象
                    3.把这个对象指向这个空间
                    A线程  132
                    B线程   在A还没有完成初始化,此时的对象有了指向,不为空,就会直接return

解决办法,加关键字 volatile ,防止指令重排

class LazzySingleTon{
    private static volatile LazzySingleTon singleTon;
    private LazzySingleTon(){}

    public  static LazzySingleTon getSingleTon(){
            if (singleTon == null){
                synchronized (LazzySingleTon.class){ 
                    if (singleTon == null){
                        singleTon = new LazzySingleTon();
                    }
                }
            }
            return singleTon;
    }
}

我们也可以采用内部类来实现单例

class StaticClsSingleTon{
    //类加载阶段:加载、连接(验证、准备(分配内存和设置默认值(静态变量))、解析)、初始化(执行clinit()方法,为static修饰的变量赋初始值和执行静态代码块的内容)
    private static StaticClsSingleTon singleTon;
    private StaticClsSingleTon(){

    }
    public  static StaticClsSingleTon getSingleTon(){
         return singleTon =  SingleTonHolder.staticClsSingleTon;
    }
    //静态内部类只有在使用时才会被加载,这里体现懒加载
    static class SingleTonHolder{
        private static StaticClsSingleTon staticClsSingleTon = new StaticClsSingleTon();
    }
}

为什么说静态内部类的实现方式是线程安全的呢

在JVM里有一个数据结构叫做SystemDictonary,这个结构主要就是用来检索我们常说的类信息,这些类信息对应的结构是klass,对SystemDictonary的理解,可以认为就是一个Hashtable,key是类加载器对象+类的名字,value是指向klass的地址。这样当我们任意一个类加载器去正常加载类的时候,就会到这个SystemDictonary中去查找,看是否有这么一个klass可以返回,如果有就返回它,否则就会去创建一个新的并放到结构里

也就是说,用同一个类加载器加载一个class文件,只会加载一次,而类的加载分为:

  • 加载
  • 连接(验证、准备(分配内存、设置默认值)、解析)
  • 初始化(类变量、静态代码块)

也就是说类变量只会被初始化一次,所以返回的是同一个对象
但是还是没有提现线程安全啊,初始化阶段,全部的类变量以及static静态代码块,都是在一个叫clinit()的方法里面完成初始化,这一点,使用jclasslib进行验证

clinit()方法是由虚拟机收集的,包含了static变量的赋值操做以及static代码块,此外,虚拟机JVM自己会保证clinit()代码在多线程并发访问的时候,只会有一个线程能够访问到,其余的线程都须要等待,而且等到执行的线程结束后才能够接着执行,但是之后不会再进入clinit()方法,因此是线程安全的

tips: 同一个类文件可以被同一个类加载器加载多次,详细可以去了解 Unsafe的defineAnonymousClass

happen-before原则

因为我们现在电脑都是多CPU,并且都有缓存,导致多线程直接的可见性问题。所以为了解决多线程的可见性问题,就有了happens-before原则,让线程之间遵守这些原则。同时,编译器还会优化我们的语句,所以等于是给了编译器优化的约束。

在Java内存模型中,happens-before 应该翻译成:前一个操作的结果可以被后续的操作获取

Happen-Before规则:
  1. 程序的顺序性规则: 在一个线程内一段代码的执行结果是有序的。就是还会指令重排,但是随便它怎么排,结果是按照我们代码的顺序生成的不会变!
  2. volatile规则: 如果一个线程先去写一个volatile变量,然后另一个线程去读这个变量,那么这个写操作的结果一定对读的这个线程可见。
  3. 传递性规则:如果A happens-before B,B happens-before C,那么A happens-before C,就是happens-before原则具有传递性。
  4. 管程中的锁规则: 无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果!(管程是一种通用的同步原语,synchronized就是管程的实现)
  5. 线程启动规则: 在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B可见。
  6. 线程终止规则: 在主线程A执行过程中,子线程B终止,那么线程B在终止之前对共享变量的修改结果在线程A中可见。
  7. 线程中断规则: 对线程interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()检测到是否发生中断
  8. 对象终结规则:一个对象的初始化的完成,也就是构造函数执行的结束一定 happens-before它的finalize()方法。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

罗罗的1024

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值