Java 单例模式
单例模式 是Java中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建 对象的最佳方式
单例模式确保在一个应用程序中某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例 单例实例。
满足条件
单例模式只应在有真正的“单一实例”的需求时才可使用:
- 单例类只能有一个实例
- 单例类必须自己创建自己的唯一实例
- 单例类必须给所有其他对象提供这一实例
两种形式
Java中实现单例模式可以通过两种形式实现:
- 懒汉模式 (类加载时不初始化)
- 饿汉模式 (在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快)
设计要求
编写单例必须满足下面的条件:
- 构造方法 变成私有
- 提供一个 静态方法 获取单实例对象
饿汉模式
饿汉模式基于classloader机制避免了 多线程 的同步问题(静态初始化将保证在任何 线程 能够访问到域之前初始化它),不过,instance在类装载时就实例化,这时候初始化instance显然没有达到懒加载(lazy loading)的效果
饿汉单例相对比较容易理解,一般表现为以下两种形式:
<pre class="prettyprint hljs java" style=" padding : 0.5em; font-family: Menlo, Monaco, Consolas, " Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin : 0px 0px 1.5em; font-size: 14px; line-height : 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">package com.shixun.design.singleton; | |
public class Singleton { | |
private static Singleton1 instance = new Singleton1(); | |
// 私有构造方法,保证外界无法直接实例化。 | |
private Singleton() { | |
} | |
// 通过公有的静态方法获取对象实例 | |
public static Singleton getInstance () { | |
return instance; | |
} | |
} |
也可以将静态对象初始化放在静态代码块中
<pre class="prettyprint hljs java" style="padding:.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">package com.shixun.design.singleton; | |
public class Singleton { | |
private static Singleton instance = null; | |
// 对象初始化放在静态代码块中 | |
static { | |
instance = new Singleton(); | |
} | |
// 私有构造方法,保证外界无法直接实例化。 | |
private Singleton() { | |
} | |
// 通过公有的静态方法获取对象实例 | |
public static Singleton getInstance() { | |
return instance; | |
} | |
} |
懒汉方式
实现单例模式能够提高类加载性能,但是和饿汉模式借助与 JVM 的类加载内部同步机制实现了线程安全不同,需要在延迟加载时注意单例实例的线程安全性,如果简单粗暴的实现,在多线程环境中将引起运行异常。
例如下面代码将引起运行异常:
<pre class="prettyprint hljs java" style="padding:.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">package com.shixun.design.singleton; | |
public class Singleton { | |
private static Singleton instance; | |
private Singleton() { | |
} | |
public static Singleton getInstance() { | |
if (instance == null) { | |
instance = new Singleton(); | |
} | |
return instance; | |
} | |
} |
上述代码多线程同时访问时可能会产生多个示例,甚至会破坏实例,违背单例的设计原则
用下面代码也能测试出来:
<pre class="prettyprint hljs java" style="padding:.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">package com.shixun.design.singleton; | |
public class SingletonTest implements Runnable{ | |
public void run() { | |
Singleton singleton3 =Singleton3.getInstance(); | |
System.out.println(singleton); | |
} | |
public static void main(String[] args) { | |
for (int i=;i<10;i++){ | |
SingletonTest myThread = new SingletonTest(); | |
Thread thread = new Thread(myThread, String.valueOf(i)); | |
Thread thread = new Thread(myThread, String.valueOf(i)); | |
Thread thread = new Thread(myThread, String.valueOf(i)); | |
thread.start(); | |
thread.start(); | |
thread.start(); | |
} | |
} | |
} |
懒汉式多线程解决方案
synchronized
可以为返回单例实例的方法设置同步用来保证线程安全性
<pre class="prettyprint hljs java" style="padding:.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">package com.shixun.design.singleton; | |
public class Singleton { | |
private static Singleton instance; | |
private Singleton() { | |
} | |
public synchronized static Singleton getInstance() { | |
if (instance == null) { | |
instance = new Singleton(); | |
} | |
return instance; | |
} | |
} |
这种写法能够在多线程中很好的工作,而且看起来它也具备很好的懒加载(lazy loading),但遗憾的是,由于整个方法被同步,因此效率相对较低
双检查锁方式
使用双检查锁需要进行两次instance == null的判断
- 第一次判断没有锁,如果install不为null直接返回单实例对象,提高效率
- 第二次判断防止多线程创建多个实例,假如A和B 两个线程同时争抢synchronized锁,A先争抢到锁,B 等待,A线程instance赋值实例化对象,释放锁,B线程获取到到锁,如果没有第二次判断的话,直接又会创建对象,那么就不符合单例要求
并且还需要为这个静态对象加上 volatile 关键字, volatile在这里的作用是:通知其他线程及时更新变量;保证有序性,禁止指令重排序。
通知其他线程及时更新变量还简单明了,第二个作用是这样的,我们举例说明一下:
原因举例说明: 在执行instance = new Singleton()语句时,一共是有三步操作的。
- 堆中分配内存
- 调用构造方法进行初始化
- 将instance引用指向内存地址。
在这三步有可能会产生指令重排序即有两种结果可能产生: 123与132(不管怎么重排序,单线程程序 的执行结果不会改变)
如果A线程执行到 instance = new Singleton() ,此时2 ,3发生重排序,选执行3,则instance已经不为 null,但是指向的对象还未初始化完成,如果此时B对象判断instance 不为null就会直接返回一个未初始 化完成的对象。
双检查锁方式代码如下:
<pre class="prettyprint hljs java" style="padding:.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">package com.shixun.design.singleton; | |
public class Singleton { | |
private volatile static Singleton instance; | |
private Singleton() { | |
} | |
// 使用双检查锁方式 | |
public static Singleton getInstance() { | |
if (instance == null) { | |
synchronized(Singleton.class){ | |
if(instance == null){ | |
instance = new Singleton(); | |
} | |
} | |
} | |
return instance; | |
} | |
} |
再测一下,没有问题:
<pre class="prettyprint hljs dart" style="padding:.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">package com.shixun.design.singleton; | |
public class SingletonTest implements Runnable{ | |
public void run() { | |
Singleton singleton5 =Singleton5.getInstance(); | |
System.out.println(singleton); | |
} | |
public static void main(String[] args) { | |
for (int i=;i<10;i++){ | |
SingletonTest myThread = new SingletonTest(); | |
Thread thread = new Thread(myThread, String.valueOf(i)); | |
Thread thread = new Thread(myThread, String.valueOf(i)); | |
Thread thread = new Thread(myThread, String.valueOf(i)); | |
Thread thread = new Thread(myThread, String.valueOf(i)); | |
Thread thread = new Thread(myThread, String.valueOf(i)); | |
thread.start(); | |
thread.start(); | |
thread.start(); | |
thread.start(); | |
thread.start(); | |
} | |
} | |
} |
静态内部类
之前提到了,静态初始化将在实例被任何线程访问到之前对其进行初始化,因此,可以借助于这个特性对懒汉单例进行改造:
静态内部类加载机制:使用时候才被加载,而且多线程情况下, classloader能够保证只加载一份字节码
代码如下:
<pre class="prettyprint hljs java" style="padding:.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">package com.shixun.design.singleton; | |
public class Singleton { | |
private static class SingletonHolder { | |
private final static Singleton Instance = new Singleton6(); | |
} | |
private Singleton() { | |
} | |
public static final Singleton getInstance() { | |
return SingletonHolder.Instance; | |
} | |
public void say() { | |
System.out.println("调用了say方法"); | |
} | |
public static void sayHello() { | |
System.out.println("调用了sayHello方法"); | |
} | |
} |
测试也OK:
<pre class="prettyprint hljs dart" style="padding:.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">package com.shixun.design.singleton; | |
public class SingletonTest implements Runnable{ | |
public void run() { | |
Singleton singleton6 =Singleton6.getInstance(); | |
System.out.println(singleton); | |
singleton.say(); | |
} | |
public static void main(String[] args) { | |
for (int i=;i<10;i++){ | |
SingletonTest myThread = new SingletonTest(); | |
Thread thread = new Thread(myThread, String.valueOf(i)); | |
Thread thread = new Thread(myThread, String.valueOf(i)); | |
Thread thread = new Thread(myThread, String.valueOf(i)); | |
Thread thread = new Thread(myThread, String.valueOf(i)); | |
Thread thread = new Thread(myThread, String.valueOf(i)); | |
thread.start(); | |
thread.start(); | |
thread.start(); | |
thread.start(); | |
thread.start(); | |
} | |
} | |
} |
枚举(别瞎用)
JDK1.5之后引入了枚举,由于枚举的特性,可以利用其来实现单例,它不仅能避免多线程同步问 题,而且还能防止反序列化重新创建新的对象(序列化和反序列化后是同一个对象)
代码如下
<pre class="prettyprint hljs java" style="padding:.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">package com.shixun.design.singleton; | |
public enum Singleton { | |
INSTANCE; | |
public void say(){ | |
System.out.println("say ni hello!"); | |
} | |
} |
测试一下:
<pre class="prettyprint hljs dart" style="padding:.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">package com.shixun.design.singleton; | |
public class SingletonTest implements Runnable{ | |
public void run() { | |
Singleton singleton7 = Singleton7.INSTANCE; | |
System.out.println(singleton.hashCode()); | |
singleton.say(); | |
} | |
public static void main(String[] args) { | |
for (int i=;i<10;i++){ | |
SingletonTest myThread = new SingletonTest(); | |
Thread thread = new Thread(myThread, String.valueOf(i)); | |
Thread thread = new Thread(myThread, String.valueOf(i)); | |
Thread thread = new Thread(myThread, String.valueOf(i)); | |
Thread thread = new Thread(myThread, String.valueOf(i)); | |
Thread thread = new Thread(myThread, String.valueOf(i)); | |
thread.start(); | |
thread.start(); | |
thread.start(); | |
thread.start(); | |
thread.start(); | |
} | |
} | |
/** | |
* 生写懒汉式多线程问题 | |
*/ private static void method() { | |
for (int i=;i<100;i++){ | |
SingletonTest myThread = new SingletonTest(); | |
Thread thread = new Thread(myThread, String.valueOf(i)); | |
Thread thread = new Thread(myThread, String.valueOf(i)); | |
Thread thread = new Thread(myThread, String.valueOf(i)); | |
thread.start(); | |
thread.start(); | |
thread.start(); | |
} | |
} | |
} |