单例设计模式
基本介绍
类的单例设计模式,就是采取一定的方式保证在整个的软件系统中,对某个类只能有一个对象实例存在,且类提供一个静态方法,用以获取该对象。
例如Hibernate的SessionFactory,它是sql会话工厂,这个对象一定是很重的(创建需要加载很多资源和时间),一般情况下,只需要一个SessionFactory就够了。
单例模式的八种实现方式
1.饿汉式(静态常量) 2.饿汉式(静态代码块) 3.懒汉式(线程不安全) 4.饿汉式(线程安全,同步方法) 5.饿汉式(线程不安全,同步代码块) 6.双重检查 7.静态内部类 8.枚举类
字体加粗的推荐使用。
饿汉式(静态常量)
实现步骤
1.构造器私有化(杜绝new) 2.在类的内部创建实例 3.在类的内部创建一个静态方法,返回创建的对象。
代码实现
class Person{
//1.通过静态属性,创建对象
private final static Person p = new Person();
//2.构造器私有化
private Person(){
}
//3.提供public 的静态方法,用以返回单例对象
public static Person getInstance(){
return p;
}
}
饿汉式(静态代码块)
代码实现
class Person{
private final static Person p;
//1.通过静态代码块,创建单例对象
static{
p = new Person();
}
//2.构造器私有化
private Person(){
}
//3.提供public 的静态方法,用以返回单例对象
public static Person getInstance(){
return p;
}
}
静态代码块和静态常量,一样效果,只是使用静态代码块,完成单例对象的创建。
饿汉式分析
优点 : 写法简单,在完成类的装载时,完成单例对象的创建,避免了线程同步问题
缺点:在类装载,就完成实例化,没有达到懒加载的效果(懒加载,就是使用时,才创建)。如果项目启动到结束,都没有使用到该单例对象,会造成内存的浪费。
这种方式基于class loader机制避免了多线程的同步问题,不过单例对象在类装载时,就完成了对象的创建。在单例模式中大多数情况下,虽然都是调用getInsatnce()方法,完成对象创建,但是
但是导致类加载的原因有多种,比如其他的静态方法被使用,导致的类加载,这时候就达不到懒加载的效果。
结论:这种单例模式可用,但是可能会造成内存浪费,且达不到懒加载效果。
懒汉式(线程不安全)
代码实现
class Person {
private static Person p;
private Person() {
}
public static Person getInstance() {
//线程不安全
if (p == null) {
p = new Person();
}
return p;
}
}
优缺点说明
- 1.起到了懒加载的效果,但是只能在单线程模式下使用。
- 2.如果在多线程下,一个线程进入了if(p == null)的判断,还没来得及往下执行,另一个线程也通过了这个判断语句,这边会产生多个实例。所以多线程环境下不要使用这种方式。
- 3.结论,开发中,不要使用这种方式。
懒汉式(线程安全,同步方法)
代码实现
class Person {
private Person() {
}
private static Person p;
//利用synchronized关键字同步方法
public static synchronized Person getInstance() {
if (p == null) {
p = new Person();
}
return p;
}
}
优缺点说明
- 1.解决了线程不安全的问题
- 2.效率低,每个线程在获取单例对象时,执行getInstance方法都要进行同步,而其实这个方法只执行一次实例代码就够了,后面的想获得该类实例,直接return就行了。方法进行同步效率太低。
- 3.结论,在实际开发中,不推荐此方式。
懒汉式(线程不安全,同步代码块)
代码实现
class Person{
private Person(){}
private static Person p;
public static Person getInstance(){
if(p == null){
synchronized(Person.class){
p = new Person();
}
}
return p;
}
}
优缺点说明
- 1.这种方式,本意是想对第四种方式的改进,因为前面的同步方法效率太低了,改为同步产生实例化的代码块
- 2.但是这种方式,并不能起到线程安全的作用,和第三种实现方式一致,假如一个线程进行了if(p == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这样就会产生多个实例
- 3.结论,实际开发,不推荐使用这种方式。
双重检查
代码实现
class Person{
private Person(){}
private static volatile Person p;//使用volatile关键字为了确保多线程环境下的可见性和有序性。
public static Person getInstance(){
if(p == null){
synchronized (Person.class){
if(p == null){
p = new Person();
}
}
}
return p;
}
}
优缺点说明
- 1.Double-Check概念,是多线程开发中常用的。如代码所示,进行两次if(p == null)的检查,就可以保证线程安全了。
- 2.这样实例化代码只执行一次,后面再次访问,判断if(p == null)直接return实例化对象,也避免了反复进行方法同步。
- 3.线程安全,延迟加载,效率高。
- 4.结论,即实现了单例模式,也满足了懒加载,效率高,且线程安全,推荐使用。
volatile关键字
在多线程环境下,每个线程都有自己的工作内存,其中包含了被线程使用的变量的副本。当一个线程修改了变量的值时,这个修改可能不会立即被其他线程看到,而是在某个时刻才会被刷新到主内存中,这就导致了可见性问题。
使用 volatile 关键字修饰一个变量,可以确保变量的修改对其他线程是可见的。当一个线程修改了 volatile 变量的值,会立即将修改后的值刷新到主内存中,其他线程在读取该变量时会从主内存中获取最新的值。
此外,volatile 关键字还可以保证变量的有序性。在多线程环境下,指令重排序可能会导致代码执行顺序与预期不符,使用 volatile 关键字修饰的变量会禁止指令重排序,保证代码的执行顺序与程序中的顺序一致。需要注意的是,volatile 关键字只能保证对单次读/写的原子性,不能保证复合操作的原子性。如果需要保证复合操作的原子性,可以使用锁或者其他同步机制。
在单例模式下的双重检查的实现方式中,需要对单例对象(静态属性)做volatile修饰,因为java中对象的生成分为 分配内存、初始化、返回对象引用 但是由于指令的无序性。即是可能会出现 分配内存 返回对象引用 初始化的指令顺序。但是是1.5之前的jdk版本会有此问题,当然概率很小。在jdk1.5后可以使用volatile关键字保证创建一个对象的指令按顺序执行。
静态内部类
class Person{
private Person(){}
private static class SubPerson{
private static final Person INSTANCE = new Person();
}
public static Person getInstance(){
return SubPerson.INSTANCE;
}
}
优缺点说明
- 1.这种方式采用了类装载机制保证初始化实例时只有一个线程。
- 2.静态内部类在Person类被装载时,并不会立即实例化,而是在getInstance方法,被调用,才会状态SubPersonl类,从而完成Person的实例化。
- 3.类的静态属性,只会在第一次加载类的时候初始化,所以这样的方式,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
- 4.优点,避免了线程不安全,利用静态内部类特定实现延迟加载,效率高。
- 5.结论 推荐使用。
枚举
代码实现
enum Person{
INSTANCE;
}
优缺点说明
- 1.这借助JDK1.5添加的枚举来实现单例模式,可以避免多线程同步问题,而且还能防止反序列化,重新创建新的对象。
- 2.结论 推荐使用,但是此方法会隐性的继承Enum类,需要注意。
单例模式的应用
java.lang.Runtime类就是经典的单例模式
注意事项和细节
- 1.单例模式保证了系统内部,一个类只存在一个对象,节省了系统资源,对于一些需要频繁创建和销毁的对象,使用单例模式可提高系统的性能。
- 2.获取单例对象,不是new,而是调用对应的获取对象的静态方法。
- 3.单例模式的使用场景,需要频繁的进行创建和销毁对象。创建对象耗时过多,或者耗费资源过多(即重量级对象),但又经常使用的对象,如频繁访问数据库,文件的对象(数据源,SessionFactory等)。
只是为了记录自己的学习历程,且本人水平有限,不对之处,请指正。。