单例模式是一种设计的模式,因为很多东西在整个应用的过程中只需要一个全局的对象。不需要多个,例如重复点击回收站,返回的是同一个对象,没有必要创建多个对象。
优点:
- 在内存中只有一个对象,节省内存空间;
- 避免频繁的创建销毁对象,可以提高性能;
- 避免对共享资源的多重占用,简化访问;
- 为整个系统提供一个全局访问点。
- 缺点:1.不适用于变化频繁的对象 ?因为频繁变化的对象在一般过程中可以通过上一层进行功能的拓展。但是对于单例模式而言,我们必须去修改对象本身。
2.单例对象的责任过重,不太符合单一职责原则。(单一职责原则指的是,我们在实际实际应用中,长将一个对象,只赋予一个职能,避免在修改的同时,影响其他职能的使用)
3.滥用单例将带来一些负面问题,如:为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;(√)
多线程操作单例对象会导致线程异常。
又比如:在多个线程中操作单例类的成员时,但单例中并没有对该成员进行线程互斥处理。
适用的场景:
- 1.需要生成唯一序列的环境。
- 2.需要频繁实例化然后销毁的对象。
- 3.创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
- 4.方便资源相互通信的环境。
例如:无状态工具类,全局信息类。
实现单例模式(7)
饿汉式和懒汉式。
首先 饿汉式的理解,就是在类加载时,先创建了对象。(主动创建)
方法1:
public class Singleton1 { private final static Singleton1 Instance = new Singleton1(); private Singleton1() { } public static Singleton1 getInstance(){ return Instance; } }
方法2:(静态代码块)
public class Singleton2 { private static Singleton2 Instance; static { Singleton2 Instance = new Singleton2(); } private Singleton2() { } public static Singleton2 getInstance(){ return Instance; } }
优点:类装载时,直接完成了实例化。避免了线程的同步问题。
缺点:如果一直没有使用,那么浪费内存。未LazyLoading
懒汉式:在需要使用这个对象时,才实例化对象。(被动创建)
注意:在懒汉式中,静态的代码只是先声明了对象,此时值为null,在计算机中未实例化的对象不占用内存。故可以节约空间。
实现单例模式
饿汉式和懒汉式。
首先 饿汉式的理解,就是在类加载时,先创建了对象。(主动创建)
public class HungrySingleton{
private static HungrySingleton create = new HungrySingleton();
private HungrySingleton(){}
public static HungrySingleton getHungrySingleton (){
return create ;
}
}
优点:类装载时,完成了实例化。避免了线程同步问题。
缺点:如果一直没有使用,那么浪费内存。未LazyLoading
懒汉式:在需要使用这个对象时,才实例化对象。(被动创建)
注意:在懒汉式中,静态的代码只是先声明了对象,此时值为null,在计算机中未实例化的对象不占用内存。故可以节约空间。
方式3:懒汉式-有线程安全问题。
/** * @program:多线程和IO * @descripton:懒汉式-线程不安全如果出现了线程竞争的情况,那么会出现两个实例。 * @author:ZhengCheng * @create:2021/9/29-16:50 **/ public class Singleton3 { private static Singleton3 Instance = null; private Singleton3() { } public static Singleton3 getInstance(){ if (Instance == null){ Instance = new Singleton3(); } return Instance; } }
我们从懒汉式单例可以看到,单例实例被延迟加载,即只有在真正使用的时候才会实例化一个对象并交给自己的引用。
这种写法起到了Lazy Loading的效果,但是只能在单线程下使用。如果在多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。
方式4:我们使用一个synchronized关键字保护getInstance,但是这样保护之后,会导致运行的速率大大下降。
/** * @program:多线程和IO * @descripton:懒汉式-线程安全-不推荐 * @author:ZhengCheng * @create:2021/9/29-16:54 **/ public class Singleton4 { private static Singleton4 Instance = null; private Singleton4(){ } public static synchronized Singleton4 getInstance(){ if (Instance == null){ Instance = new Singleton4(); } return Instance; } }
方式5:那么,用synchronized包着方法效率太低,那我们包着创建对象的代码,是不是就提高了效率。
/** * @program:多线程和IO * @descripton:懒汉式-线程不安全 * @author:ZhengCheng * @create:2021/9/29-16:56 **/ public class Singleton5 { private static Singleton5 Instance = null; private Singleton5(){ } public static Singleton5 getInstance(){ if (Instance == null){ 阻塞1 synchronized (Singleton5.class){ Instance = new Singleton5(); } } return Instance; } }
上述的代码看似很安全,但是,实际上是不安全的!!!
因为假设有两个线程,他们同时通过了判断,此时Instance确实是null,那么现在他们都来到阻塞1,开始抢锁。第一个线程拿到了锁,创建对象,第二个线程之后也会拿到锁,再次创建对象,这就有了两个对象,线程是不安全的。所以我们需要使用Doublecheck的方式,来保证线程安全问题。
方式6:Double-Check 推荐使用
public class Singletin6 { private volatile static Singletin6 Instance = null; private Singletin6(){ } public static Singletin6 getInstance(){ if (Instance == null){ synchronized (Singletin6.class){ if (Instance == null){ Instance = new Singletin6(); } } } return Instance; } }
从方式6中,我们看到,创建了该单例模式,我们使用了两次判断,以解决方式5中出现的问题。解决了上述问题,那为什么我在声明Instance还使用了Volatile,这是为什么呢?
原因在于避免重排序的影响。首先,我们知道,给除了long和double等基本类型赋值是原子性的,给引用类型赋值也是原子性的,但是,我们现在在赋值时,需要new一个对象。这个操作,并不是原子性的。这个赋值存在三个操作:1.创建一个空白对象 2.调用构造方法实例化对象 3.赋值。如果发生了重排序,使得2.3交换了运行顺序,假设Thread1刚好赋值成功,切到Thread2,Thread2拿到锁一判断,发现对象不是空的,那么就会跳过创建语句,直接return。那么return了以后,Thread2发现被坑了,拿到的实例是空的,于是会出现空指针异常。
所以为了保证不发生重排序,我们使用volatile关键字来对Instance进行保护。
方式7:使用静态内部类的方法(线程安全,总体还不错,但是会提高代码的复杂度)
/** * @program:多线程和IO * @descripton:使用静态内部类的方法 目前是懒汉!并且Safe 推荐 * @author:ZhengCheng * @create:2021/9/30-9:28 **/ public class Singleton7 { private Singleton7(){ } private static class SingletonInstance{ private static final Singleton7 Instance = new Singleton7(); } public static Singleton7 getInstance(){ return SingletonInstance.Instance; } }
方式8:最佳实现单例模式的方法,枚举类
/** * @program:多线程和IO * @descripton:枚举单例模式最佳写法 * 使用枚举类类似静态类,直接调用。 * @author:ZhengCheng * @create:2021/9/30-9:42 **/ public enum Singleton8 { Instance;//创建枚举实例 public void doWhatUWant(){ } }
在《Effective Java》中:“使用枚举实现单例的方法虽然还没有广泛采用,但是段素的枚举类型已经成为实现Singleton的最佳方法”
♦ 写法简单 ♦线程安全 ♦ 避免反序列化破坏单例
各种写法的适用场合
♦ 最好的方法是利用枚举
♦ 不安全的方法不使用
♦ 如果程序一开始要加载的资源太多,应当适用懒加载
♦ 饿汉式如果对象的创建需要配置文件就不适用。
♦ 懒加载虽然好,但是静态内部类的方式会引入编程复杂性
面试常见问题:
♦ 饿汉式的缺点?
♦ 懒汉式的缺点?
♦ 为什么使用Double-Check?不用就不安全吗?
♦ 为什么双重检查模式需要Volatile
♦ 应该如何选择,用哪种单例的实现方式最好?
总体复习问题:
1.JMM应用实例:8种单例模式,单例和并发的关系
2.什么是JMM
3.volatile和synchronized的异同?
4.什么是原子操作?Java有哪些原子操作?生成对象的过程是不是原子操作?
5.什么是内存可见性?
6.64位long和double写入的时候是原子的吗?