单例模式
学习目标:
1.单例的概念和作用
2.实现单例的五种方式
3.各种单例的优缺点分析
4.了解反射破环单例
5.防止反射破环单例
这篇文章看完,在单例问题上足够应付多数面试单例问题。
单例作用:保证一个类只有一个实例,并且提供一个访问该实例的全局访问点
详解:前半句话的意思是把一个类设置为Static,并且把类设置为私有。这样别人无法new这个类,保证只有一个实例。后半句话给该类设置一个public get()方法。这样我们就可以拿到这个实例。
场景:windows的任务管理器和windows的回收站,他们在计算机上只能打开一个窗口。
读取配置文件的类,读一次加载到文件内存中,我们直接使用就可以了。
数据库连接池的设计,在Spring在,每个Bean默认都是单例的。
优点:单例模式只生成一个实例,减少了系统性能开销。
单例模式可以在系统中设置全局访问点,优化共享资源访问。
常见的五种实现方式:
1.饿汉式(线程安全,调用效率高,不能延时加载)
2.懒汉式(线程安全,调用效率不高,可以延时加载)
3.DCL懒汉式,双重检锁(由于JVM底层内部模型原因,偶尔会出现问题,不建议使用)
4.饿汉式改进:静态内部类式(线程安全,调用效率高,可以延时加载)
5.枚举单例(线程安全,调用效率高,不能延时加载)
类装载发生的时机:
1.实例化对象时,就像spring管理的bean一样,在tomcat启动时就实例化了bean,那么这个对象bean的类就加载了。
2.通过类名调用静态变量的时候(类名.class除外)
static修饰的变量和方法理解:
1、被static修饰的变量属于类变量,可以通过类名.变量名直接引用,而不需要new出一个类来
2、被static修饰的方法属于类方法,可以通过类名.方法名直接引用,而不需要new出一个类来
被static修饰的变量、被static修饰的方法统一属于类的静态资源,是类实例之间共享的.
饿汉式单例
//饿汉式单例
public class singleton {
//私有化构造器
private singleton(){
}
//类初始化的时候,立即加载该对象。
//static变量会在类装载的时候就初始化,不会引发并发问题。
//static变量不依赖singleton类特定的实例,被singleton类的所有实例共享。
//只要singleton类被加载,Java虚拟机就能根据类名在运行时数据区的方法区内定找到他们
private static singleton instance = new singleton();
//提供获取该对象的方法,没有synchronized,效率高。
//用public修饰的static成员变量和成员方法本质是全局变量和全局方法
public static singleton getInstance(){
return instance;
}
}
class singletonTest{
public static void main(String[] args) {
singleton instance = singleton.getInstance();
singleton instance1 = singleton.getInstance();
System.out.println(instance==instance1);
}
}
测试结果
饿汉式单例弊端
private static singleton instance = new singleton();这段代码说明用Static修饰的singleton变量,在类加载的时候就会变成静态资源,而且实例化了singleton类。如果我们在这里给它开辟了空间。就造成了资源浪费。我们利用懒汉式来解决该问题。
懒汉式单例
synchronized关键字修饰说明:
假如有多个线程同时访问。如果在不加synchronized关键字的时候,线程A去判断instance是否为空,如果为空,则去实例化这个singleton类。就在此刻线程B进来,它也判断instance变量为空,则也去实例化。无法实现单例。所以使用synchronized关键字,让线程排队去访问该方法。有synchronized关键字,效率低。
//懒汉式单例
public class singleton {
private singleton(){
}
//类初始化的时候,不立即加载该对象
private static singleton instance;
//synchronized 同步该方法。防止线程不安全,效率较低。
public static synchronized singleton getInstance(){
if(instance==null){
instance = new singleton();
}
return instance;
}
}
测试结果
类初始化 线程安全 效率 弊端
饿汉式 加载类对象,无并发。 安全 高 浪费资源空间
懒汉式 不加载 安全(synchronized) 低 效率低
懒汉式弊端:
懒汉式单例保证了懒加载和线程安全问题,但是使用了synchronized关键字,效率比较低。需要优化synchronized关键字。所以出现新的单例模式--双重检测单例(DCL懒汉式)
双重检测单例
不需要同步整个方法,锁的范围更精细。如果线程A刚进来发现instance对象没创建,它要和其他线程竞争本类的锁,获得锁之后线程A才会执行里面的代码,这时候再次检测,instance确实为空,说明自己是第一个竞争到这个锁的,这个线程A就负责创建这个对象了,如果检测不是null了,说明这份锁已经被别人用过了,对象也已经创建出来了,以后创建对象直接用就好了。线程A创建实例后直接释放锁,并返回实例对象。
volatile作用:
禁止进行指令重排序。
可见性
DCL懒汉式:通过volatile关键字确保线程安全。
第二次检测后,去创建对象时,对象实例化过程非原子的。
1.在内存中开辟一片内存区域。
2.在这片区域中执行构造函数,实例化对象。
3.instance引用指向这片内存区域。
假设instance变量没有用volatile关键字修饰,A,B俩线程并发访问singleton类中getInstance()方法,线程A先执行,假设它拿到锁对象,并且判断二次检测为空,则实例化这个对象。上述三步操作可能被指令重排序为1-->3-->2。当线程A执行了3,还未执行2,或者2未完全执行完,此时instance不再是null(instance所指向区域已分配内存),若此时轮到了线程B去拿实例,在第一次判断instance,这是非null.就将instance返回了。此时instance对象并未实例化或者并未完全实例化,必将发生错误。
如果instance变量被volatile修饰,俩层效果。
1.禁止指令重排序优化,即不会出现指令未1->3->2的情况,只能是1->2->3.当线程B执行第一次检测判断,instance要么为null,要么指向已完全初始化了的对象。
2.保证instance变量的可见性,当线程A成功初始化实例后,会将实例从线程A工作内存区域刷新到内存。同时其他线程工作内存区域中的instance无效化,使得其他线程只能从主内存中获取instance对象,
详情原因:https://blog.csdn.net/csdnwgf/article/details/96126654
//双重检测(DCL)懒汉式加载
public class singleton {
private singleton(){
}
//类初始化的时候,不立即加载该对象
private volatile static singleton = instance;
//synchronized 同步该代码块。锁的更加精细化
public static singleton getInstance(){
if(instance==null){
synchronized(singleton.class){
if(instance==null){
instance = new singleton();
}
}
}
return instance;
}
}
静态内部类单例:
//静态内部类实现
public class singleton {
private singleton(){
}
//创建私有的静态内部类
//我们在静态内部类中实例化对象,如果我们没有调用getInstance()方法,它这里不会被加载的。
//这种方法线程安全,因为instance被static finnal修饰,保证了内存中只有一个实例的存在
//兼容了并发下的高效率调用,解决了延迟加载
private static class InnerClass{
private static final singleton instance = new singleton();
}
public static singleton getInstance(){
return InnerClass.instance;
}
}
class singletonTest{
public static void main(String[] args) {
singleton instance = singleton.getInstance();
singleton instance1 = singleton.getInstance();
System.out.println(instance==instance1);
}
}
测试结果:
利用反射破环静态内部类
利用反射拿出它的无参构造方法,破环掉无参构造的private修饰符,利用无参构造对象.newInstance()获取其对象。通过这个对象和正常从静态内部法单例中获取的对象进行hashCode比较。
public class singleton {
private singleton(){
}
//创建私有的静态内部类
//我们在静态内部类中实例化对象,如果我们没有调用getInstance()方法,它这里不会被加载的。
//这种方法线程安全,因为instance被static finnal修饰,保证了内存中只有一个实例的存在
//兼容了并发下的高效率调用,解决了延迟加载
private static class InnerClass{
private static final singleton instance = new singleton();
}
public static singleton getInstance(){
return InnerClass.instance;
}
}
class singletonTest{
public static void main(String[] args) throws Exception{
singleton instance = singleton.getInstance();
Constructor<singleton> declaredConstructor = singleton.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
singleton singleton = declaredConstructor.newInstance();
System.out.println(instance == singleton );
System.out.println(instance.hashCode());
System.out.println(instance.hashCode());
}
}
演示结果
设置标记位解决反射破环单例问题
这段代码非常有趣,我们需要很明确的理解到new对象,就需要调用无参构造。使用静态内部类方法,它只会进行一次new对象,以后就是直接返回instance,标记位flag设置在无参构造中,有效保证了实例只能被new一次。
public class singleton {
private static Boolean flag = false;
private singleton(){
if(flag==false){
flag=true;
}else {
throw new RuntimeException("不要试图用反射破环我们的单例");
}
}
//创建私有的静态内部类
//我们在静态内部类中实例化对象,如果我们没有调用getInstance()方法,它这里不会被加载的。
//这种方法线程安全,因为instance被static finnal修饰,保证了内存中只有一个实例的存在
//兼容了并发下的高效率调用,解决了延迟加载
private static class InnerClass{
private static final singleton instance = new singleton();
}
public static singleton getInstance(){
return InnerClass.instance;
}
}
class singletonTest{
public static void main(String[] args) throws Exception{
singleton instance = singleton.getInstance();
// singleton instance1 = singleton.getInstance();
Constructor<singleton> declaredConstructor = singleton.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
singleton singleton = declaredConstructor.newInstance();
// Constructor<singleton> declaredConstructor1 = singleton.class.getDeclaredConstructor();
// declaredConstructor1.setAccessible(true);
// singleton singleton1 = declaredConstructor1.newInstance();
System.out.println(instance == singleton );
System.out.println(instance.hashCode());
System.out.println(singleton.hashCode());
}
}
测试结果
枚举单例
public enum singleton {
INSTANCE;
public static singleton getInstance(){
return INSTANCE;
}
public static void main(String[] args) {
singleton instance = singleton.getInstance();
singleton instance1 = singleton.getInstance();
System.out.println(instance==instance1);
}
}
测试结果
总结常见的五种实现方式:
1.饿汉式(线程安全,调用效率高,不能延时加载)
饿汉式单例里面没有开辟大量空间,可以用这种方式。
2.懒汉式(线程安全,调用效率不高,可以延时加载)
假设单例中开辟很多内存空间,因为它可以延时加载。可以使用这种模式。但是效率较低。
3.DCL懒汉式,双重检锁(由于JVM底层内部模型原因,偶尔会出现问题,不建议使用)
优化了懒汉式同步方法,把它变为同步块,加volatile关键字,保证它的原子性,有序性,一致性。
4.饿汉式改进:静态内部类式(线程安全,调用效率高,可以延时加载)
静态内部类单例最优秀的单例,兼容了并发下的高效率调用,解决了延时加载,但以上四种都可以被反射破环。
5.枚举单例(线程安全,调用效率高,不能延时加载)
枚举单例式为纯天然单例,而且反射不能破环它,因为一用,程序就抛异常了。但是它不能延时加载。