单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一,这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象,也就是说不能使用new关键字来创建对象。
1.单例模式的实现
单例设计模式分类两种: 饿汉式:类加载就会导致该单实例对象被创建。 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建。
1.1 饿汉式
饿汉式-方式1(静态变量方式)
这种方法通过将对象的实例设置为静态的方式,保证了该对象的实例,永远只有一份,且该对象的创建在类加载的时候就会立即创建在jvm内存中的方法区,在程序运行期间永久存在,所以当我们的对象太大的时候就会造成一种资源的浪费。
代码示例:
/**
* 饿汉式
* 静态变量创建类的对象
*/
public class Singleton {
//私有构造方法
private Singleton() {}
//在成员位置创建该类的对象
private static Singleton instance = new Singleton();
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return instance;
}
}饿汉式-方式2(静态代码块方式)
在方式2中,对象的创建是在静态代码块中,也是对着类的加载而创建。所以和饿汉式的方式1基本上一样。
/**
* 饿汉式-方法2
* 在静态代码块中创建该类对象
*/
public class Singleton {
//私有构造方法
private Singleton() {}
//在成员位置创建该类的对象
private static Singleton instance;
static {
instance = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return instance;
}
}1.2 懒汉式
懒汉式-方式1(线程不安全)
从名字就可以看出二者的区别,饿汉就是一直处于饿的状态,需要不断有食物给你,也就是对象一直存在。
而懒汉式,就比较懒惰,只有真正饿的时候才会寻找食物,也就是请求对象实例.
所以,当在以下代码中只要调用getInstance()方法获取Singleton类的对象的时候才创建Singleton类的对象,这样就实现了懒加载的效果。
但是,如果是多线程环境下,每个线程抢占Singleton类的对象资源,但是可能会发生对个线程同时请求对象实例的问题,这个时候就有可能创建多个对象,从而导致数据不一致,就会出现线程安全问题。
/**
* 懒汉式
* 线程不安全
*/
public class Singleton {
//私有构造方法
private Singleton() {}
//在成员位置创建该类的对象
private static Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}懒汉式-方式2(线程安全)
所以对方式1的线程不安全,我们在方式2中进行了优化,通过加同步锁的机制,保证了每次只有一个线程可以操作我们当前的对象,确保了线程安全,
但是由于加锁就会导致该代码执行效率特别低。从上面代码我们可以看出,其实就是在初始化instance的时候才会出现线程安全问题,一旦初始化完成就不存在了,所以基于此,我们做进一步优化。
/**
* 懒汉式
* 线程安全
*/
public class Singleton {
//私有构造方法
private Singleton() {}
//在成员位置创建该类的对象
private static Singleton instance;
//对外提供静态方法获取该对象,并且加锁
public static synchronized Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}懒汉式-方式3(双重检查锁)
再来讨论一下懒汉模式中加锁的问题,对于 getInstance() 方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以我们没必要让每个线程必须持有锁才能调用该方法,我们需要调整加锁的时机。由此也产生了一种新的实现模式:双重检查锁模式;
/**
* 双重检查方式
*/
public class Singleton {
//私有构造方法
private Singleton() {}
private static Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
if(instance == null) {
synchronized (Singleton.class) {
//抢到锁之后再次判断是否为null
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}在双重检查锁模式下,为什么要进行两次的判断呢?
现在我们假设有两个线程a,b。两个线程都去请求我们单例模式下类的实例,
当第一个判断的时候,两个线程都会进入判断代码块中进行锁的抢占,最终a抢占到了锁,那么b只能在加锁的代码块外部进行等候,这个时候a创建了对象的实例,完成功能后归还了锁,这个时候线程b马上抢占到了锁,然后进入内部代码块
假设在这里没有第二次判断的话,线程a就会再次创建一个新的对象,所以,要在这里再加一次判断。
双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,但是呢,JVM在实例化对象的时候会进行优化和指令重排序操作,在多线程的情况下,就可能会出现空指针问题
具体的细节不再叙述,大概进行一些讲解; 对于上述对象的创建主要分为三个部分:
分配对象的内存空间。
初始化对象。
设置instance指针指向对象所在的内存空间。
为了提高性能在JVM在实例化对象的时候会进行优化和指令重排序操作,也就是说有可能将我们上述的第七行和第九行代码进行顺序的交换。 也就是说当我们上面a线程得到锁以后,这时候还没有初始化我们的对象,就先设置了instance指针指向了我们的对象所在的内存空间,
a线程在设置了instance指针指向了我们的对象所在的内存空间以后就归还了锁,线程b这个时候拿到锁以后,检查到对象不为空,直接返回了线程a创建的对象,但是这个时候线程a还没有完成对象的初始化,所以就导致了线程b拿到的对象是一个空对象,就会出现空指针的问题。
那么如何解决上述的问题呢,只需要使用 volatile 关键字, volatile 关键字可以保证可见性和有序性,这个关键字禁止了对当前修饰的变量上下文重排序。保证了方法的可靠性。
代码示例:
/**
* 双重检查方式
*/
public class Singleton {
//私有构造方法
private Singleton() {}
//使用volatile修饰,禁止重排序
private static volatile Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实际
if(instance == null) {
synchronized (Singleton.class) {
//抢到锁之后再次判断是否为空
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}懒汉式-方式4(静态内部类方式)
静态内部类单例模式中实例由内部类创建,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性。静态属性由于被 static 修饰,保证只被实例化一次,并且严格保证实例化顺序。
/**
* 静态内部类方式
*/
public class Singleton {
//私有构造方法
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}说明: 第一次加载Singleton类时不会去初始化INSTANCE,只有第一次调用getInstance,虚拟机加载SingletonHolder 并初始化INSTANCE,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。
所以静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。
补充:饿汉式-方式3 (枚举方式)
枚举类实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚 举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。
/**
* 枚举方式
*/
public enum Singleton {
INSTANCE;
}没错,正如你所看到,枚举正是你所看到的这样就是这么简单,实际上枚举类型是在Java的语法糖。
扩展:
在计算机科学中,语法糖(syntactic sugar)是指编程语言中可以更容易的表达一个操作的语法,它可以使程序员更加容易去使用这门语言:操作可以变得更加清晰、方便,或者更加符合程序员的编程习惯,总的来说语法糖是为了我们更加方便的简单的书写代码,有没有语法糖我们都可以实现类似的功能。
在此我们可以对枚举方式实现的实例进行反编译,可以得到如下的代码:
public final class Singleton extends Enum
{
public static Singleton[] values()
{
return (Singleton[])$VALUES.clone();
}
public static Singleton valueOf(String name)
{
return (Singleton)Enum.valueOf(com/qgn/mianshi/main/Singleton, name);
}
private Singleton(String s, int i)
{
super(s, i);
}
public static final Singleton INSTANCE;
private static final Singleton $VALUES[];
static
{
INSTANCE = new Singleton("INSTANCE", 0);
$VALUES = (new Singleton[] {
INSTANCE
});
}
}