实现Singleton 模式
目录
8 拓展问题: 定义一个表示总统的类型President,可以从改类型继承出FrechPresident和AmericanPresident, 这些派生类只能产生一个实例。
1 “懒汉式”与“饿汉式”的区别
所谓“懒汉式”与“饿汉式”的区别,是在与建立单例对象的时间的不同。
“懒汉式”
“懒汉式”是在你真正用到的时候才去建这个单例对象:
- 私有化构造器
- 创建一个私有的实例static 先不实例化 为 null
- 通过公共方法调用 static 在方法里面进行判断,if = null
实例化 !=null 直接return
比如:有个单例对象
public class Singleton{
private Singleton(){}
private static Singleton singleton = null; //不建立对象
public static synchronized Singleton getInstance(){
if(singleton == null) { //先判断是否为空
singleton = new Singleton (); //懒汉式做法
}
return singleton ;
}
}
“饿汉式”
“饿汉式”是在不管你用的用不上,一开始就建立这个单例对象:比如:有个单例对象
:一个类只能创建一个对象
- 私有化构造器
- 在类的内部创建一个类的实例,且为static
- 私有化对象,通过公共方法调用
- 此公共方法只能通过类来调用,因为设置的是static,同时类的实例也是static
public class Singleton{
public Singleton(){}
private static Singleton singleton = new Singleton(); //建立对象
public static Singleton getInstance(){
return singleton ;//直接返回单例对象
}
}
2 GetInstance与new区别:
new的使用:如Object _object = new Object(),这时候,就必须要知道有第二个Object的存在,而第二个Object也常常是在当前的应用程序域中的,可以被直接调用的
GetInstance的使用:在主函数开始时调用,返回一个实例化对象,此对象是static的,在内存中保留着它的引用,即内存中有一块区域专门用来存放静态方法和变量,可以直接使用,调用多次返回同一个对象。
3 实现Singleton 模式 7种方式
方式1 饿汉,常用
首先是写明私有的构造方法防止被new,然后直接就实例化,最后调用,不存在线程安全问题。
/**
* 单例模式,饿汉式,线程安全
*/
public static class Singleton1{
private final static Singleton instance = new Singleton();
private Singleton1(){
}
public static Singleton getInstance(){
return instance;
}
方式2 懒汉,不安全
可能会出现线程安全问题,因为new一个对象在JVM底层做了如下工作:
1 给 instance 分配内存
2 调用 Singleton 的构造函数来初始化成员变量
3 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)
显然是不能保证原子性的。
单线程的时候工作正常,但在多线程的情况下就有问题了。设想如果两个线程同时运行到判断instance是否为null的 if语句,并且instance的确没有创建时,那么两个线程都会创建一个实例,此时类型Singletonl就不再满足单例模式的要求了。为了保证在多线程环境下我们还只能得到类型的一个实例,需要加上一个同步锁。把 Singletonl稍作修改
/**
* 单例模式,懒汉式,线程不安全
*/
public static class Singleton2 {
private static Singleton2 instance = null;
private Singleton2() {
}
public static Singleton2 getInstance() {
if (instance == null) {
instance = new Singleton2();
}
return instance;
}
}
方式3 加锁的懒汉,性能低
我们还是假设有两个线程同时想创建一个实例。由于在一个时刻只有一个线程能得到同步锁,当第一个线程加上锁时,第二个线程只能等待。当第一个线程发现实例还没有创建时,它创建出一个实例。接着第一个线程释放同步锁,此时第二个线程可以加上同步锁,并运行接下来的代码。这时候由于实例己经被第一个线程创建出来了,第二个线程就不会重复创
建实例了,这样就保证了我们在多线程环境中也只能得到一个实例。但是类型Singleton2 还 是 很 完 美 。我们每次通过属性Instance得到Singleton2 的实例,都会试图加上一个同步锁,而加锁是一个非常耗时的操作,在没有必要的时候我们应该尽量避免。
/**
* 单例模式,懒汉式,线程安全,多线程环境下效率不高
*/
public static class Singleton3 {
private static Singleton3 instance = null;
private Singleton3() {
}
public static synchronized Singleton3 getInstance() {
if (instance == null) {
instance = new Singleton3();
}
return instance;
}
}
方式4 -静态块,可以
参考地址https://www.cnblogs.com/fengzheng/p/9103227.html
当第一次引用getInstance()方法的时候,访问静态内部类中的静态成员变量,此时该内部类需要调用static代码块(因为首次访问该类)。而后再次访问getInstance()方***直接返回instace引用。这种做法相对于传统做法更加巧妙。
/**
* 单例模式,懒汉式,变种,线程安全
*/
public static class Singleton4 {
private static Singleton4 instance = null;
static {
instance = new Singleton4();
}
private Singleton4() {
}
public static Singleton4 getInstance() {
return instance;
}
}
方式5 静态内部类 推荐
定义一个私有的内部类,在第一次用这个嵌套类时,会创建一个实例。而类型为SingletonHolder的类,只有在Singleton.getInstance()中调用,由于私有的属性,他人无法使用SingleHolder,不调用Singleton.getInstance()就不会创建实例。
优点:达到了lazy loading的效果,即按需创建实例。
/**
* 单例模式,使用静态内部类,线程安全【推荐】
*/
public static class Singleton5 {
private final static class SingletonHolder {
private static final Singleton5 INSTANCE = new Singleton5();
}
private Singleton5() {
}
public static Singleton5 getInstance() {
return SingletonHolder.INSTANCE;
}
}
虚拟机会保证一个类的类构造器<clinit>()在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造器<clinit>(),其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。
特别需要注意的是,在这种情形下,其他线程虽然会被阻塞,但如果执行<clinit>()方法的那条线程退出后,其他线程在唤醒之后不会再次进入/执行<clinit>()方法,因为 在同一个类加载器下,一个类型只会被初始化一次。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个线程阻塞,在实际应用中这种阻塞往往是隐藏的
方式 6枚举,推荐
/**
* 静态内部类,使用枚举方式,线程安全【推荐】
*/
public enum Singleton06{
INSTANCE;
}
在《Effective Java》最后推荐了这样一个写法,简直有点颠覆,不仅超级简单,而且保证了线程安全。这里引用一下,此方法无偿提供了序列化机制,绝对防止多次实例化,及时面对复杂的序列化或者反射攻击。单元素枚举类型已经成为实现Singleton的最佳方法。
很多人会对枚举法实现的单例模式很不理解。这里需要深入理解的是两个点:
1 枚举类实现其实省略了private类型的构造函数
2枚举类的域(field)其实是相应的enum类型的一个实例对象
对于第一点实际上enum内部是如下代码:
public enum Singleton {
INSTANCE;
// 这里隐藏了一个空的私有构造方法
private Singleton () {}
}
比较清楚的写法是:
public class SingletonExample5 {
private SingletonExample5(){}
public static SingletonExample5 getInstance(){
return Singleton.INSTANCE.getInstance();
}
private enum Singleton{
INSTANCE;
private SingletonExample5 singleton;
//JVM保证这个方法绝对只调用一次
Singleton(){
singleton = new SingletonExample5();
}
public SingletonExample5 getInstance(){
return singleton;
}
}
}
为什么能保证线程安全
https://www.cnblogs.com/z00377750/p/9177097.html
方式7 . 双重校验锁 推荐
思想:先判断一下是不是null,然后加锁,再判断一下是否为null。如果还是null,则可以放心地new。
还是不大明白为什么要判断两次null?
第二次校验:如果没有第二次校验,假设线程t1执行了第一次校验后,判断为null,这时t2也获取了CPU执行权,也执行了第一次校验,判断也为null。接下来t2获得锁,创建实例。这时t1又获得CPU执行权,由于之前已经进行了第一次校验,结果为null(不会再次判断),获得锁后,直接创建实例。结果就会导致创建多个实例。所以需要在同步代码里面进行第二次校验,如果实例为空,则进行创建。
/**
* 静态内部类,使用双重校验锁,线程安全【推荐】
*/
public static class Singleton7 {
private volatile static Singleton7 instance = null;
private Singleton7() {
}
public static Singleton7 getInstance() {
if (instance == null) {
synchronized (Singleton7.class) {
if (instance == null) {
instance = new Singleton7();
}
}
}
return instance;
}
}
执行过程
双重校验锁方式的执行过程如下:
1.线程A进入 getInstance() 方法。
2.由于 singleton为 null,线程A在 //1 处进入 synchronized 块。
3.线程A被线程B预占。
4.线程B进入 getInstance() 方法。
5.由于 singleton仍旧为 null,线程B试图获取 //1 处的锁。然而,由于线程A已经持有该锁,线程B在 //1 处阻塞。
6.线程B被线程A预占。
7.线程A执行,由于在 //2 处实例仍旧为 null,线程A还创建一个 Singleton 对象并将其引用赋值给 instance。
8.线程A退出 synchronized 块并从 getInstance() 方法返回实例。
9.线程A被线程B预占。
10.线程B获取 //1 处的锁并检查 instance 是否为 null。
11.由于 singleton是非 null 的,并没有创建第二个 Singleton 对象,由线程A所创建的对象被返回。
主函数
public static void main(String[] args) {
System.out.println(Singleton.getInstance() == Singleton.getInstance());
System.out.println(Singleton2.getInstance() == Singleton2.getInstance());
System.out.println(Singleton3.getInstance() == Singleton3.getInstance());
System.out.println(Singleton4.getInstance() == Singleton4.getInstance());
System.out.println(Singleton5.getInstance() == Singleton5.getInstance());
System.out.println(Singleton6.INSTANCE == Singleton6.INSTANCE);
System.out.println(Singleton7.getInstance() == Singleton7.getInstance());
}
8 拓展问题: 定义一个表示总统的类型President,可以从改类型继承出FrechPresident和AmericanPresident, 这些派生类只能产生一个实例。
/**
* 问题拓展: 定义一个表示总统的类型President,可以从改类型继承出FrechPresident和AmericanPresident,
* 这些派生类只能产生一个实例。
*/
class President {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
class FrenchPresident extends President {
private FrenchPresident() {
}
private static class Nested {
private final static FrenchPresident instance = new FrenchPresident();
}
public static FrenchPresident getInstance() {
return Nested.instance;
}
public static void main(String[] args) {
FrenchPresident s1 = FrenchPresident.getInstance();
FrenchPresident s2 = FrenchPresident.getInstance();
System.out.println(s1.hashCode());
System.out.println(s2.hashCode());
}
}
class AmericanPresident {
private AmericanPresident() {
}
public static AmericanPresident getInstance() {
return Nested.instance;
}
private static class Nested {
public static final AmericanPresident instance = new AmericanPresident();
}
public static void main(String[] args) {
AmericanPresident s1 = AmericanPresident.getInstance();
AmericanPresident s2 = AmericanPresident.getInstance();
System.out.println(s1.hashCode());
System.out.println(s2.hashCode());
}
}
在上述的代码中,我们在内部定义了一个私有类型Nested。当第一次用到这个嵌套类型的时候,会调用静态构造函数创建FrenchPresident 以及AmericanPresident的实例instance。类型Nested只在属性FrenchPresident.Instance与AmericanPresident.Instance中被用到,由于其私有属性,他人无法使用Nested类型。因此,当我们第一次试图通过属性 FrenchPresident.Instance与AmericanPresident.Instance得到FrenchPresident和AmericanPresident的实例时,会自动调用Nested的静态构造函数创建实例instance。如果我们不调用属性Singleton5.Instance,就不会调用Nested, 也不会创建实例,这样就真正做到了按需创建。.