单例模式 —————确保对象的唯一性!!!!
Java 中最简单的设计模式之一,这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决:一个全局使用的类频繁地创建与销毁。
何时使用:当您想控制实例数目,节省系统资源的时候。
如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
关键代码:构造函数是私有的。
简单的应用实例:
1、Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。
2、一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。
一、演示使用
一、SingletonPatternDemo是SingleObject的调用类,以下是演示在使用时是怎么去调用单例模式的类
public class SingletonPatternDemo {
public static void main(String[] args) {
//不合法的构造函数
//编译时错误:构造函数 SingleObject() 是不可见的
//SingleObject object = new SingleObject();
//获取唯一可用的对象
SingleObject object = SingleObject.getInstance();
//显示消息,调用方法
object.showMessage();
}
}
public class SingleObject {
//创建 SingleObject 的一个对象
private static SingleObject instance = new SingleObject();
//让构造函数为 private,这样该类就不会被实例化
private SingleObject(){}
//获取唯一可用的对象
public static SingleObject getInstance(){
return instance;
}
public void showMessage(){
System.out.println("Hello World!");
}
}
二、单例模式的五种实现方式
ps:“单例模式”应该和“volatile”、“static”关键字联系起来
3个重要的衡量点:
1)延迟加载(lazy loading)
2)线程安全(多线程操作时保证对象的唯一性)
3)反射、反序列
懒汉式与饿汉式的根本区别在与是否在类内方法外创建自己的对象。
并且声明对象都需要私有化,构造方法都要私有化,这样外部才不能通过 new 对象的方式来访问。
饿汉式的话是声明并创建对象(因为他饿),懒汉式的话只是声明对象,在调用该类的 getinstance() 方法时才会进行 new 对象。
//1、懒汉式,线程不安全
/**
不支持多线程。没有加锁(synchronized),可以完全不考虑
**/
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
//2、懒汉式,线程安全
/**
优点:具备很好的 lazy loading,能够在多线程中很好的工作,第一次调用才初始化,避免内存浪费
缺点:效率很低,加锁会影响效率
不考虑,如果要使用可以进一步使用双重锁定,提高效率
**/
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
//3、饿汉式
/**
优点:没有加锁,执行效率会提高。
缺点:类加载时就初始化,浪费内存。容易产生垃圾对象,instance 在类装载时就实例化,没有达到 lazy loading 的效果
常用
**/
public class Singleton {
// 这句就是饿汉模式的核心
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
//4、双检锁/双重校验锁(DCL,即 double-checked locking)
/**
优点:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
双检锁方式可实例域需要延迟初始化时使用
**/
public class Singleton {
//双检锁/双重校验锁,需要增加 volatile 关键字,禁止指令重排序:
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
//5、登记式/静态内部类
/**
原理:Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。
优点:对静态域使用延迟初始化
**/
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
//6、枚举
/**
优点:枚举实现可以很好地解决反射和反序列化会破坏单例模式的问题,提供了一种更加安全和可靠的单例模式实现机制。该方法作为单例模式的最佳实现方法。
Java虚拟机会保证枚举对象的唯一性,因此每一个枚举类型和定义的枚举变量在JVM中都是唯一的。
**/
public enum Singleton {
INSTANCE;
public void whateverMethod() {
System.out.println("我是一个单例!");
}
}
//将一个已有的类改造为单例类,也可以使用枚举的方式来实现。
public class Singleton {
private Singleton(){
}
//在其中增加一个内部枚举类型来存储和创建它的唯一实例即可,这和前面的静态内部类的实现有点相似
public static enum SingletonEnum {
SINGLETON;
//定义了枚举类型的实例对象Singleton,其初始值为null
private Singleton instance = null;
private SingletonEnum(){
instance = new Singleton();
}
public Singleton getInstance(){
return instance;
}
}
}
2-1、单例模式中instance为什么一定要static?
1)通过静态的类方法(getInstance)获取instance,该方法是静态方法,instance由该方法被返回(被该方法使用),如果instance非静态,无法被getInstance调用
//获取唯一可用的对象,这也是为什么getInstance()是静态方法,因为要对象唯一性,只有这样指向的都才是同一个对象
SingleObject object = SingleObject.getInstance();
2)instance需要在调用getInstance前被初始化,只有static的成员变量才能在没有创建对象时初始化且类的成员变量在类第一次被使用时初始化,就不会再初始化,保证了单例。
3)static类型的instance存在静态存储区,每次调用时都指向了同一个对象。
反射机制破解单例模式(枚举除外):
public class BreakSingleton{
public static void main(String[] args) throw Exception{
Class clazz = Class.forName("Singleton");
Constructor c = clazz.getDeclaredConstructor(null);
c.setAccessible(true);
Singleton s1 = c.newInstance();
Singleton s2 = c.newInstance();
//通过反射,得到的两个不同对象
System.out.println(s1);
System.out.println(s2);
}
}
如何避免以上(反射)的漏洞:
class Singleton{
private static final Singleton singleton = new Singleton();
private Singleton() {
//在构造器中加个逻辑判断,多次调用抛出异常
if(instance!= null){
throw new RuntimeException()
}
}
public static Singleton getInstance(){
return singleton;
}
}
如何避免实现序列化单例模式的漏洞:
class Singleton implements Serializable{
private static final Singleton singleton = new Singleton();
private Singleton() {
}
public static Singleton getInstance(){
return singleton;
}
//反序列化定义该方法,则不需要创建新对象
private Object readResolve() throws ObjectStreamException{
return singleton;
}
}
三、补充:
Ⅰ、volatile关键字
一般主要讨论的是:synchronized和volatile关键字的区别
1)volatile的本质上是告诉JVM当前变量的值是不确定的,需要得到及时的更新,所以会把它放到工作内存当中,而synchronized会锁定该变量,只有当前线程可以访问,其他线程会被阻塞。volatile不会造成线程阻塞,synchronized则有可能会出现。
2)volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
3)volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
当且仅当满足以下所有条件时,才应该使用volatile变量:
1、对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
2、该变量没有包含在具有其他变量的不变式中。
总结:在需要同步的时候,第一选择应该是synchronized关键字,这是最安全的方式,尝试其他任何方式都是有风险的。尤其在、jdK1.5之后,对synchronized同步机制做了很多优化,如:自适应的自旋锁、锁粗化、锁消除、轻量级锁等,使得它的性能明显有了很大的提升。
个人问题点:单例模式的双重锁定模式为什么需要用volatile屏蔽指令重排序?
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) { // 1
synchronized(Singleton.class) {
if (singleton == null) {
singleton = new Singleton(); // 2
}
}
}
return singleton;
}
实际上当程序执行到2处的时候,如果我们没有使用volatile关键字修饰变量singleton,就可能会造成错误。这是因为使用new关键字初始化一个对象的过程并不是一个原子的操作,它分成下面三个步骤进行:
a. 给 singleton 分配内存
b. 调用 Singleton 的构造函数来初始化成员变量
c. 将 singleton 对象指向分配的内存空间(执行完这步 singleton 就为非 null 了)
如果虚拟机存在指令重排序优化,则步骤b和c的顺序是无法确定的。如果A线程率先进入同步代码块并先执行了c而没有执行b,此时因为singleton已经非null。这时候线程B到了1处,判断singleton非null并将其返回使用,因为此时Singleton实际上还未初始化,自然就会出错。synchronized可以解决内存可见性,但是不能解决重排序问题。
重排序:(7条消息) 什么是指令重排序?为什么要重排序?_HCH996的博客-CSDN博客_为什么会发生指令重排序
Ⅱ、static关键字
static :可以修饰内部类,方法,字段。
修饰内部类:被static修饰 的内部类可以直接作为一个普通的类来使用,而不需先实例一个外部列。
修饰方法:调用该方法的时候只需要类名 . 方法就可以直接调用,不需要创建对象。
static方法一般称作静态方法,由于静态方法不依赖于任何对象就可以进行访问,因此对于静态方法来说,是没有 this
的,因为它不依附于任何对象,既然都没有对象,就谈不上this` 了。
修饰字段:通过类名 . 的方式可以直接 获取 或者 赋值。
static变量也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。