狂神的单例模式是真的讲的非常好理解,这里附上链接:
【狂神说Java】单例模式-23种设计模式系列_哔哩哔哩_bilibili
单例模式就是让一个类只能创建一个对象
单例中最重要的就是构造方法私有(一旦构造方法私有,别人就无法new这个对象了)
是23种设计模式中最简单的
目录
单例模式的概念及优缺点
单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
单例模式包含角色:Singleton
这样的模式有几个好处:
1、某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销。节约系统资源
2、省去了new操作符,降低了系统内存的使用频率,减轻GC压力。
3、有些类如交易所的核心交易引擎,控制着交易流程,如果该类可以创建多个的话,系统完全乱了。
缺点:由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。在一定程度是违背了“开闭原则”。
在以下情况下可以使用单例模式:
单例模式分类
饿汉式单例:类加载就会导致该单实例对象被创建
懒汉式单例:类加载不会导致改单实例对象被创建,而是首次使用该对象时才会创建
饿汉式单例
一开始就将所有东西全部加载进来,相当于对象已经存在了,非常占用内存资源
实现方式1(静态变量方式)
//饿汉式单例
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(){
}
//在本类中创建本类对象
private final static Hungry HUNGRY = new Hungry();
//提供一个公共的访问方式,让外界获取该对象
public static Hungry getInstance(){
return HUNGRY;
}
}
//测试
public class Client{
public static void main(String []args){
//创建Hungry类的对象
Hungry hungry = Hungry.getInstance();
Hungry hungry1 = Hungry.getInstance();
//判断获取到的两个是否是同一个对象
//利用==号,比较的是两个对象在内存中的位置
System.out.println(hungry == hungry1);
//结果为true,保证了Hungry类只能创建一个对象
}
}
实现方式2(静态代码块)
public class Hungry{
//私有构造方法
private Hungry(){}
//声明Hungry类型的变量,之所以是静态是因为要通过类名直接访问
private static Hungry instance;//初始值为null
//在静态代码块中进行赋值
static {
instance = new Hungry();
}
//对外提供获取该类对象的方法
public static Hungry getInstance(){
return instance;
}
}
//测试
public class Client{
public static void main(String []args){
//创建Hungry类的对象
Hungry hungry = Hungry.getInstance();
Hungry hungry1 = Hungry.getInstance();
//判断获取到的两个是否是同一个对象
//利用==号,比较的是两个对象在内存中的位置
System.out.println(hungry == hungry1);
//结果为true,保证了Hungry类只能创建一个对象
}
}
懒汉式单例
对象是用的时候才去加载,而不是一开始就加载
方式1(线程不安全)
//懒汉式单例
public class LazyMan{
//私有构造方法
private LazyMan(){
}
//声明LazyMan类型的变量
private static LazyMan LAZYMAN;//只是声明,没有创建
//对外提供访问方式
public static LazyMan getInstance(){
//判断是否创建了LAZYMAN对象
if(LAZYMAN == null){
LAZYMAN = new LazyMan();
}
return LAZYMAN;
}
}
//测试
public class Client{
public static void main(String[] args){
LazyMan instance = LazyMan.getInstance();
LazyMan instance1 = LazyMan.getInstance();
//判断两次获取到的LazyMan对象是否是同一个对象
System.out.println(instance == instance1);
//结果为ture
}
}
这个方式的单线程下是OK的,在多线程并发中是会出现问题的:
//懒汉式单例
public class LazyMan{
private LazyMan(){
//打印线程名称
System.out.println(Thread.currentThread().getName());
}
private static LazyMan LAZYMAN;
public static LazyMan getInstance(){
//线程1等待,线程2获取CPU的执行权,也会进到该判断中
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:线程安全
针对多线程,添加锁模式synchronized,缺点:执行效果特别低
//懒汉式单例
public class LazyMan{
//私有构造方法
private LazyMan(){
}
//声明LazyMan类型的变量
private static LazyMan LAZYMAN;//只是声明,没有创建
//对外提供访问方式
public static synchronized LazyMan getInstance(){
//判断是否创建了LAZYMAN对象
if(LAZYMAN == null){
LAZYMAN = new LazyMan();
}
return LAZYMAN;
}
}
方式3:双重检测模式的懒汉式单例(DCL懒汉式)
懒汉式方式2中加锁的问题,对于getInstance()方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以没必要让每个线程必须持有锁才能调用该方法,我们需要调整加锁的时机,由此出现了:双重检查锁模式。
volatile:保证可见性和有序性
注意:volatile和synchronized的区别:前者保证有序性(变量的创建),后者保证原子性(同步)
volatile能保证线程的可见性,但是并不能替代synchronized保证原子性
ps:原因是volatile无法保证线程安全问题,比如i++操作,在底层是需要三步完成的(cpu读取内存中的值——cpu将值+1——cpu将值写回内存)。假设一个场景:i = 0,线程A先执行,要写回时CPU资源被线程B抢夺,此时线程A没有执行完成,线程B取到的还是i = 1,然后执行i++并写回(i=2),假设这个时候线程A又被执行,由于之前已经读取过值了,就不会再从内存中读取,而是直接通过线程的上下文切换机制,获取到的就是线程A之前没有执行完毕的i值,i = 1,然后执行完毕并写回内存,结果依旧是i=1,线程不安全
之所以去加volatile的原因就是因为实例化时不是原子性操作,避免指令重排
//懒汉式单例,双重检查锁方式
public class LazyMan{
private LazyMan(){
//打印线程名称
System.out.println(Thread.currentThread().getName());
}
private volatile static LazyMan LAZYMAN;
public static LazyMan getInstance(){
//第一次判断,如果值不为null,不需要抢占锁,直接返回对象
if(LAZYMAN == null){
synchronized (LazyMan.class){
//第二次判断
if(LAZYMAN == null) {
LAZYMAN = new LazyMan();//不是原子性操作
/**
* 会进行的操作:
* 1,分配内存空间
* 2,执行构造方法,初始化对象
* 3,将这个对象指向这个空间
* 容易出现顺序乱的情况,比如132等
*
* 添加volatile关键字和synchronized加锁
*/
}
}
}
return LAZYMAN;
}
//多线程并发
public static void main(String[] args){
for(int i=0;i<10;i++){
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
}
方式4:静态内部类实现(推荐)
在本方式中,实例右内部类创建,由于jvm在加载外部类的过程中,是不会加载静态内部类的,只有内部类的属性、方法别调用时才会被下载,并初始化其静态属性。
静态属性由于被static修饰,保证只被实例化一次,并且严格保证实例化顺序
//静态内部类方式
public class Holder{
//私有构造方法
private Holder() {
}
//定义一个静态内部类
private static class InnerClass{
//在内部类中创建外部类对象
private static final Holder HOLDER = new Holder();//final防止外部对它进行修改
}
//获取实例
public static Holder getInstance() {
return InnerClass.HOLDER;
}
}
//测试
public class Client{
public static void main(String[] args){
Holder instance = Holder.getInstance();
Holder instance1 = Holder.getInstance();
//判断两次获取到的LazyMan对象是否是同一个对象
System.out.println(instance == instance1);
//结果为ture
}
}
反射(单例不安全)
破坏单例模式就是创建多个对象
上述单例实现方式都是不安全的,只要有反射,任何代码都是不安全的
反射:可以通过class模板得到你写的所有方法和属性
在懒汉式单例中
import java.lang.reflect.Constructor;
//懒汉式单例
public class LazyMan{
private LazyMan(){
}
private volatile static LazyMan LAZYMAN;
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();
//添加null,无参构造器
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
//setAccessible是一个非常霸道的方法,会无视私有的构造器,通过反射来创建对象
declaredConstructor.setAccessible(true);
LazyMan instance2 = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(instance2);
}
}
可以看到结果是已经破坏了。
解决方式
在私有的构造方法中加锁,不允许破坏
private LazyMan(){
synchronized (LazyMan.class) {
if(LAZYMAN != null){
//抛一个运行时异常
throw new RuntimeException("不要试图使用反射破坏异常");
}
}
}
再次运行就会报错
但是如果两次都用反射破坏呢?这要怎么解决呢?可以设置一个“红绿灯”,也就是定义一个都不知道的布尔变量,外部通过反射的情况是找不到这个变量的。
import java.lang.reflect.Constructor;
//懒汉式单例
public class LazyMan{
//设置一个“红绿灯”
private static boolean pipisong = false;
private LazyMan(){
synchronized (LazyMan.class) {
if(pipisong == false) {
pipisong = true;
}else {
throw new RuntimeException("不要试图使用反射破坏异常");
}
}
}
private volatile static LazyMan LAZYMAN;
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{
//添加null,无参构造器,获取无参构造方法对象
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
//setAccessible是一个非常霸道的方法,会无视私有的构造器,通过反射来创建对象
declaredConstructor.setAccessible(true);
LazyMan instance = declaredConstructor.newInstance();
LazyMan instance2 = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(instance2);
}
}
实际上,别人也可以获取到我们设置的“红绿灯”,并且将其破坏掉。所以程序没有办法做到绝对的安全,所谓“道高一尺,魔高一丈”。
Field pipisong = LazyMan.class.getDeclaredField("pipisong");
pipisong.setAccessible(true);
枚举enum与反射_A person,A fool的博客-CSDN博客
所以为了单例安全,我们可以考虑使用枚举,但是需要注意的是枚举中没有无参构造,而是有两个参数(String,int)
序列化和反序列化也能破坏单例
首先我们要知道什么是序列化和反序列化:
序列化:把对象转换为字节序列的过程称为对象的序列化
反序列化:把字节序列恢复为对象的过程称为对象的反序列化
测试一下序列化破坏单例
//测试用反射破坏单例
//以静态内部类为例
//Holder类要实现序列化接口Serializable
public class Client{
public static void main(String[] args) throws Exception{
writeObject2File();
readObjectFromFile();
readObjectFromFile();
}
//向文件中写数据(对象)
public static void writeObject2File() throws Exception{
//1、获取单例对象
Holder instance = Holder.getInstance();
//2、创建对象输出流对象,该路径会生成一个文件
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("文件路径"));
//3、写对象
oos.writeObject(instance);
//4、释放资源
oos.close();
}
//从文件中读数据(对象)
public static void readObjectFromFile() throws Exception{
//1、创建对象输出流对象
ObjectInputStream ois = new ObjectInputStream(new FileOutputStream("文件路径"));
//2、读取对象
Holder instance = (Holder)ois.readObject();
System.out.println(instance);//如果输出的两次打印的对象地址是否一样,如果一样说明没有破坏单例模式,如果不一样说明破坏了单例模式
//结果输出是不一样的,已经破坏了
//3、释放资源
ois.close();
}
}
解决方式
可以在Holder类中添加readResolve方法解决这个问题:
/* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */
public Object readResolve() {
return getInstance();
}
解释:在源码中,ObjectinputStream类中,可以看到readOrdinaryObject()方法调用的是hasReadResolveMethod()判断是否有readResolve()方法,如果有,就执行这个方法,如果没有这个方法,就new一个新对象
枚举方式实现单例模式(恶汉式)最优
枚举类型是线程安全的,并且只会装载一次。是所有单例实现中,唯一一种不会被破坏的单例实现模式。
//枚举:写法简单
public enum Singleton{
INSTANCE;
}
//测试
public class Client{
public static void main(String[] args){
Singleton instance = Singleton.INSTANCE;
Singleton instance1 = Singleton.INSTANCE;
//判断两次获取到的LazyMan对象是否是同一个对象
System.out.println(instance == instance1);
//结果为ture
}
}
在不考虑内存空间的情况下,首选枚举方式实现单例模式