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