单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
- 1、单例类只能有一个实例。
- 2、单例类必须自己创建自己的唯一实例。
- 3、单例类必须给所有其他对象提供这一实例。
介绍
意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决: 一个全局使用的类频繁地创建与销毁。
何时使用: 当您想控制实例数目,节省系统资源的时候。
如何解决: 判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
关键代码: 构造函数是私有的。
饿汉式
上来直接新建对象!
package com.codefriday.Single;
//饿汉式单例模式
public class Hungry {
//可能造成空间浪费
private byte[] data1 = new byte[1024*1024];
private byte[] data2 = new byte[1024*1024];
private byte[] data3 = new byte[1024*1024];
private byte[] data4 = new byte[1024*1024];
//构造器私有
private Hungry(){}
//加载类时就new出对象
private final static Hungry HUNGRY = new Hungry();
public static Hungry getInstance(){
return HUNGRY;
}
}
静态内部类实现单例模式
package com.codefriday.Single;
public class Holder {
private Holder(){}
private static class innerClass{
private static Holder INSTANCE = new Holder();
}
public static Holder getInstance(){
return innerClass.INSTANCE;
}
}
/**
* 静态内部类的优点是:外部类加载时并不需要立即加载内部类,
* 内部类不被加载则不去初始化INSTANCE,故而不占内存。
* 即当Holder第一次被加载时,并不需要去加载innerClass,
* 只有当getInstance()方法第一次被调用时,才会去初始化INSTANCE,
* 第一次调用getInstance()方法会导致虚拟机加载innerClass类,
* 这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
*/
懒汉式
对象用的时候再加载。
(1)有问题的版本
package com.codefriday.Single;
//懒汉式单例
public class LazyMan {
private static LazyMan lazyman = null;
//构造器私有
private LazyMan(){
System.out.println(Thread.currentThread().getName()+"OK");
}
public static LazyMan getInstance(){
//用到时才创建对象
if(lazyman==null){
lazyman = new LazyMan();
}
return lazyman;
}
//问题:多线程下会破坏单例模式
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
}
(2)DCL懒汉式,仍然存在问题
package com.codefriday.Single;
//懒汉式单例
public class LazyMan {
private static LazyMan lazyman = null;
//构造器私有
private LazyMan(){
System.out.println(Thread.currentThread().getName()+"OK");
}
//双重检测锁模式,Double-check-lock,DCL懒汉式
public static LazyMan getInstance(){
//用到时才创建对象
if(lazyman==null){
synchronized (LazyMan.class){
if(lazyman==null) {
lazyman = new LazyMan();
}
}
}
return lazyman;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
}
lazyman = new LazyMan();
不是原子操作,分以下几个步骤
- 分配内存空间
- 执行构造方法初始化对象
- 对象指向这个空间
解决方案:private static LazyMan lazyman = null;
加volatile
修饰
(3)反射可以破坏单例模式
反射获得私有构造器,设置可到达,然后newinstance
可以通过构造器新建一个对象。
package com.codefriday.Single;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
//懒汉式单例
public class LazyMan {
private static LazyMan lazyman = null;
//构造器私有
private LazyMan(){
System.out.println(Thread.currentThread().getName()+"OK");
}
//双重检测锁模式,Double-check-lock,DCL懒汉式
public static LazyMan getInstance(){
//用到时才创建对象
if(lazyman==null){
synchronized (LazyMan.class){
if(lazyman==null) {
lazyman = new LazyMan();
}
}
}
return lazyman;
}
public static void main(String[] args) throws Exception {
LazyMan instance = LazyMan.getInstance();
//获得单例类的构造器
Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
LazyMan instance2 = constructor.newInstance();
System.out.println(instance);
System.out.println(instance2);
}
}
解决方案:在构造器中加一个判断,如果已经被实例,抛出异常。
(4)加判断避免反射
package com.codefriday.Single;
import java.lang.reflect.Constructor;
//懒汉式单例
public class LazyMan {
private static LazyMan lazyman = null;
//构造器私有
private LazyMan(){
synchronized (LazyMan.class){
if(lazyman!=null){
throw new RuntimeException("不要试图用反射新建对象!!!");
}
}
}
//双重检测锁模式,Double-check-lock,DCL懒汉式
public static LazyMan getInstance(){
//用到时才创建对象
if(lazyman==null){
synchronized (LazyMan.class){
if(lazyman==null) {
lazyman = new LazyMan();
}
}
}
return lazyman;
}
public static void main(String[] args) throws Exception {
LazyMan instance = LazyMan.getInstance();
//获得单例类的构造器
Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
LazyMan instance2 = constructor.newInstance();
System.out.println(instance);
System.out.println(instance2);
}
}
运行结果:
仍然存在问题:
不使用getInstance
方法对象就不会被创建,使用反射获得构造器就可以获得两次实例破坏单例模式!
解决方案:
增加一个标志位,可以当做一个密码位,调用构造器时修改密码位即可。实现代码如下。
(5)密码位判断
package com.codefriday.Single;
import java.lang.reflect.Constructor;
//懒汉式单例
public class LazyMan {
private boolean password = false;
private static LazyMan lazyman = null;
//构造器私有
private LazyMan(){
synchronized (LazyMan.class){
if(password){
password = true;
}else{
throw new RuntimeException("不要试图用反射新建对象!!!");
}
}
}
//双重检测锁模式,Double-check-lock,DCL懒汉式
public static LazyMan getInstance(){
//用到时才创建对象
if(lazyman==null){
synchronized (LazyMan.class){
if(lazyman==null) {
lazyman = new LazyMan();
}
}
}
return lazyman;
}
public static void main(String[] args) throws Exception {
//LazyMan instance = LazyMan.getInstance();
//获得单例类的构造器
Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
LazyMan instance1 = constructor.newInstance();
LazyMan instance2 = constructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
问题: 即使标志位经过加密处理,也可能被解密。通过反编译。
解决方案:
查看newInstance
的源码如下:
发现如果是枚举类型就不能破坏单例模式。
枚举实现单例模式
package com.codefriday.Single;
public enum SingleEnum {
INSTANCE;
public SingleEnum getInstance(){
return INSTANCE;
}
}
class Test{
public static void main(String[] args) {
System.out.println(SingleEnum.INSTANCE);
}
}
但是发现编译完的class文件中有一个私有构造方法:
尝试用反射获取破坏一下:
package com.codefriday.Single;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public enum SingleEnum {
INSTANCE;
public SingleEnum getInstance(){
return INSTANCE;
}
}
class Test{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor<SingleEnum> constructor = SingleEnum.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
SingleEnum instance = constructor.newInstance();
System.out.println(instance);
}
}
运行结果:
按道理应该抛出的是newInstance
方法中的Cannot reflectively create enum objects
异常的,然而却抛出一个找不到构造方法的异常。
使用javap反编译一下:
发现也存在一个空参构造器。使用更专业的反编译工具!
JAD下载地址:https://varaneckas.com/jad/
查看源码如下:
存在一个有参构造,因此我们可以获得该构造器。
测试代码:
package com.codefriday.Single;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public enum SingleEnum {
INSTANCE;
public SingleEnum getInstance(){
return INSTANCE;
}
}
class Test{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor<SingleEnum> constructor = SingleEnum.class.getDeclaredConstructor(String.class,int.class);
constructor.setAccessible(true);
SingleEnum instance = constructor.newInstance();
SingleEnum instance1 = constructor.newInstance();
System.out.println(instance);
System.out.println(instance1);
}
}
运行结果:达到预期