目录
- 1.结构
- 2.实现
- 2.1.饿汉式
- 2.1.1.静态变量
- 2.1.2.静态代码块
- 2.1.3.枚举方式
- 2.2.懒汉式
- 2.2.1.synchronized 线程安全
- 2.2.2.双重检查锁
- 2.2.3.静态内部类方式
- 3.破坏单例模式
- 3.1.序列化反序列化
- 3.2.反射
- 4.问题解决
- 5.JDK 源码解析——Runtime 类
1.结构
(1)单例模式 (Singleton Pattern) 是 Java 中最简单的设计模式之一。它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
(2)单例模式的主要有单例类和访问类这两个角色:
单例类 | 只能创建一个实例的类 |
---|---|
访问类 | 使用单例类 |
2.实现
单例模式分类两种:饿汉式和懒汉式。
饿汉式 | 类加载就会导致该单实例对象被创建 |
---|---|
懒汉式 | 类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建 |
2.1.饿汉式
2.1.1.静态变量
package com.itheima.patterns.singleton.hungryman1;
//饿汉式
public class Singleton{
//私有构造方法
private Singleton(){}
//静态变量创建类的对象
private static Singleton instance = new Singleton();
//对外提供静态方法获取该对象
public static Singleton getInstance(){
return instance;
}
}
说明: 方式一在成员位置声明 Singleton 类型的静态变量,并创建 Singleton 类的对象 instance。instance 对象是随着类的加载而创建的。如果该对象足够大的话,而一直没有使用就会造成内存的浪费。
2.1.2.静态代码块
package com.itheima.patterns.singleton.hungryman2;
public class Singleton{
//私有构造方法
private Singleton(){}
//在静态代码块中进行创建
private static Singleton instance;
static {
instance = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance(){
return instance;
}
}
说明:方式二在成员位置声明Singleton类型的静态变量,而对象的创建是在静态代码块中,也是随着类的加载而创建。所以和方式一基本一样,也存在内存浪费问题。
2.1.3.枚举方式
package com.itheima.patterns.singleton.hungryman3;
public enum Singleton {
INSTANCE;
}
说明:枚举类实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。测试代码如下:
Client.java
package com.itheima.patterns.singleton.hungryman;
public class Client {
public static void main(String[] args) {
Singleton instance1 = Singleton.INSTANCE;
Singleton instance2 = Singleton.INSTANCE;
// instance1 和 instance2 为同一个对象,结果为 true
System.out.println(instance1 == instance2);
}
}
2.2.懒汉式
2.2.1.synchronized 线程安全
package com.itheima.patterns.singleton.Lazyman;
public class Singleton{
//私有构造方法
private Singleton(){}
//声明 Singleton 类型的变量 instance,并未进行赋值
private static Singleton instance;
//使用关键字 synchronized 的目的在于保证线程安全
public static synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
说明:该方式实现了懒加载效果,同时又解决了线程安全问题。但是在 getInstance() 方法上添加了 synchronized 关键字,导致该方法的执行效果特别低。从上面代码我们可以看出,其实就是在初始化 instance 的时候才会出现线程安全问题,一旦初始化完成就不存在了。
2.2.2.双重检查锁
package com.itheima.patterns.singleton.lazyman2;
public class Singleton {
//私有构造方法
private Singleton(){}
//声明 Singleton 类型的变量 instance,并未进行赋值
private static volatile Singleton instance;
public static Singleton getInstance(){
//第一次检查,若 instance 不为 null,则不进入抢锁阶段,直接返回实际值即可
if (instance == null) {
synchronized(Singleton.class){
//第二次检查,得到锁之后再次判断 instance 是否为空
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
说明:双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,虽然双重检测锁模式看上去完美无缺,其实是存在问题,在多线程的情况下,可能会出现空指针问题,出现问题的原因是 JVM 在实例化对象的时候会进行优化和指令重排序操作。要解决双重检查锁模式带来空指针异常的问题,只需要使用 volatile 关键字,volatile 关键字可以保证可见性和有序性。 添加 volatile 关键字之后的双重检查锁模式是一种比较好的单例实现模式,能够保证在多线程的情况下线程安全也不会有性能问题。
2.2.3.静态内部类方式
package com.itheima.patterns.singleton.lazyman3;
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.破坏单例模式
破坏单例模式演示(序列化反序列化和反射)
3.1.序列化反序列化
package com.itheima.patterns.singleton.problem1;
import java.io.Serializable;
public class Singleton implements Serializable {
//私有构造方法
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
package com.itheima.patterns.singleton.problem1;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class Client {
public static void main(String[] args) throws Exception {
writeObject2File();
readObjectFromFile();
readObjectFromFile();
}
//从文件读取数据(对象)
public static void readObjectFromFile() throws Exception {
//1.创建对象输入流对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("E:\\testData\\a.txt"));
//2.读取对象
Singleton instance = (Singleton) ois.readObject();
System.out.println(instance);
//释放资源
ois.close();
}
//向文件中写数据(对象)
public static void writeObject2File() throws Exception {
//1.获取Singleton对象
Singleton instance = Singleton.getInstance();
//2.创建对象输出流对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("E:\\testData\\a.txt"));
//3.写对象
oos.writeObject(instance);
//4.释放资源
oos.close();
}
}
3.2.反射
public class Singleton {
//私有构造方法
private Singleton() {}
private static volatile Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
if(instance != null) {
return instance;
}
synchronized (Singleton.class) {
if(instance != null) {
return instance;
}
instance = new Singleton();
return instance;
}
}
}
package com.itheima.patterns.singleton.problem2;
import java.lang.reflect.Constructor;
public class Client {
public static void main(String[] args) throws Exception {
//获取 Singleton 类的字节码对象
Class clazz = Singleton.class;
//获取 Singleton 类的私有无参构造方法对象
Constructor constructor = clazz.getDeclaredConstructor();
//取消访问检查
constructor.setAccessible(true);
//创建 Singleton 类的对象 s1
Singleton s1 = (Singleton) constructor.newInstance();
//创建 Singleton 类的对象 s2
Singleton s2 = (Singleton) constructor.newInstance();
//判断通过反射创建的两个 Singleton 对象是否是同一个对象
System.out.println(s1 == s2); // false
}
}
4.问题解决
(1)序列化、反序列化方式破坏单例模式的解决方法
在 Singleton 类中添加 readResolve() 方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新 new 出来的对象。
package com.itheima.patterns.singleton.reslove1;
import java.io.Serializable;
public class Singleton implements Serializable {
//私有构造方法
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
//解决序列化反序列化破解单例模式
private Object readResolve() {
return SingletonHolder.INSTANCE;
}
}
具体的深入分析可以参考这篇文章。
(2)反射方式破解单例的解决方法
当通过反射方式调用构造方法进行创建创建时,直接抛异常,不运行此种操作。
package com.itheima.patterns.singleton.reslove1;
import java.io.Serializable;
public class Singleton implements Serializable {
private static boolean flag = false;
//私有构造方法
private Singleton() {
synchronized (Singleton.class){
//若 flag 的值为 true,说明不是第一次访问,直接抛一个异常
if(flag){
throw new RuntimeException("不能创建多个对象!");
}
flag = true;
}
}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
//解决序列化反序列化破解单例模式
private Object readResolve() {
return SingletonHolder.INSTANCE;
}
}
5.JDK 源码解析——Runtime 类
(1)Runtime 类就是使用的单例设计模式,其部分源代码如下:
public class Runtime {
private static Runtime currentRuntime = new Runtime();
/**
* Returns the runtime object associated with the current Java application.
* Most of the methods of class <code>Runtime</code> are instance
* methods and must be invoked with respect to the current runtime object.
*
* @return the <code>Runtime</code> object associated with the current
* Java application.
*/
public static Runtime getRuntime() {
return currentRuntime;
}
/** Don't let anyone else instantiate this class */
private Runtime() {}
...
}
从上面源代码中可以看出 Runtime
类使用的是饿汉式(静态属性)方式来实现单例模式的。
(2)使用 Runtime 类
package com.itheima.patterns.singleton.runtimedemo;
import java.io.IOException;
import java.io.InputStream;
public class RunTimeDemo {
public static void main(String[] args) throws IOException {
//获取RunTime类对象
Runtime runtime = Runtime.getRuntime();
System.out.println("JVM 空闲内存 =" + runtime.freeMemory() / (1024*1024) + "M");
System.out.println("JVM 总内存 =" + runtime.totalMemory() / (1024*1024) + "M");
System.out.println("JVM 可用最大内存 =" + runtime.maxMemory() / (1024*1024) + "M");
//调用 runtime 的方法exec,参数为一个命令
Process process = runtime.exec("ipconfig");
//调用 process 对象的获取输入流的方法
InputStream is = process.getInputStream();
byte[] arr = new byte[1024 * 1024 * 100];
int length = is.read(arr);
System.out.println(new String(arr,0,length, "GBK"));
}
}