单例模式

简介

应用场景: 单例模式为了保证对象的唯一性, 在一些业务场景中, 只需要创建一个对象实例, 如: 线程池 连接池对象等, 这时候可以使用单例模式来解决该问题.
首先单例模式的构造方法必须为private, 以此避免使用着实例化对象.
其次必须有个方法获取类实例.
存储和获取实例必须使用static关键字, 如果不使用static关键字的话, 想要调用/获取方法中的实例 必须是以new的方式才能进行的, 并且被static关键字修饰过的变量只会在类加载时进行初始化, 这样就保证了实例的唯一性, 这样就违背了单例模式的初衷, 所以要使用static关键字来保存或获取实例

饿汉式

所谓饿汉式就是指无论需不需要使用类实例, 在首次进行类加载时,就将类实例初始化好.
这样做的缺点是占用内存空间且是线程安全的.

饿汉式0.1

使用static关键字修饰的成员变量只会在类加载时进行初始化, 一个类只会加载以此, 保证了实例的唯一 运行以下代码输出地址一致 由此可知是一个对象

public class Main {
    public static void main(String[] args) {
        HungryLyra lyra = HungryLyra.lyra;
        HungryLyra heartstrings = HungryLyra.lyra;
        System.out.println(lyra + "--------" + heartstrings);

    }
}


class HungryLyra {
    public static HungryLyra lyra = new HungryLyra();
}

image
但是这样做也有个缺点, 由于实例中的static修饰的变量是public作用域的, 因此该成员变量会被外部进行修改操作, 比如以下代码, 直接将成员变量修改为null, 这样做显然是不合法的

public class Main {
    public static void main(String[] args) {
//        HungryLyra lyra = HungryLyra.getLyra();
//        HungryLyra heartstrings = HungryLyra.getLyra();
//
//        System.out.println(lyra + "--------" + heartstrings);

        HungryLyra lyra = HungryLyra.lyra;
        HungryLyra.lyra = null;
        HungryLyra heartstrings = HungryLyra.lyra;
        System.out.println(lyra + "--------" + heartstrings);

    }
}

class HungryLyra {
    public static HungryLyra lyra = new HungryLyra();
}

image

饿汉式1.0

方法1 使用private修饰成员变量

将成员变量属性设置为private, 并暴露一个方法用于获取实例. 想要获取实例时需要调用该方法, 这样做就避免了实例的二次修改操作

public class Main {
    public static void main(String[] args) {
        HungryLyra lyra = HungryLyra.getLyra();
        HungryLyra heartstrings = HungryLyra.getLyra();
        System.out.println(lyra + "--------" + heartstrings);

    }
}

class HungryLyra {
    private static HungryLyra lyra = new HungryLyra();

    public static HungryLyra getLyra() {
        return lyra;
    }
}

image

方法2 使用final修饰成员变量 避免实例被修改

被final修饰的成员变量为常量, 常量不允许进行修改, 以此来解决问题.

public class Main {
    public static void main(String[] args) {
//        HungryLyra lyra = HungryLyra.getLyra();
//        HungryLyra heartstrings = HungryLyra.getLyra();
//
//        System.out.println(lyra + "--------" + heartstrings);

        HungryLyra lyra = HungryLyra.lyra;
        HungryLyra heartstrings = HungryLyra.lyra;
        System.out.println(lyra + "--------" + heartstrings);

    }
}

class HungryLyra {
    public final static HungryLyra lyra = new HungryLyra();
}

懒汉式

所谓懒汉式就是只在使用实例时才会对实例进行初始化操作.

懒汉式0.1

在使用时对成员变量进行初始化, 很显然这并不是单例模式, 每次调用方法都会进行new对象, 并无法保证实例的唯一性

public class Main {
    public static void main(String[] args) {
        HungryLyra lyra = HungryLyra.getLyra();
        HungryLyra heartstrings = HungryLyra.getLyra();

        System.out.println(lyra + "--------" + heartstrings);


    }
}

class HungryLyra {
    private static HungryLyra lyra = null;

    public static HungryLyra getLyra() {
        lyra = new HungryLyra();
        return lyra;
    }
}

image

懒汉式0.2

添加判空操作, 当成员变量为空时, 才会进行初始化, 这样保证了实例的唯一性.

public class Main {
    public static void main(String[] args) {
        HungryLyra lyra = HungryLyra.getLyra();
        HungryLyra heartstrings = HungryLyra.getLyra();

        System.out.println(lyra + "--------" + heartstrings);


    }
}

class HungryLyra {
    private static HungryLyra lyra = null;

    public static HungryLyra getLyra() {
        if (lyra == null) {
            lyra = new HungryLyra();
        }
        return lyra;
    }
}

image
但是这样做还是有问题, 在多线程环境下, 无法保证线程安全.
线程允许时, 线程一首先判空操作, 这时候成员变量是空的, 进行实例化对象时, 被线程2抢去了运行权, 由于还没有对成员变量进行实例化, 导致实例化了2次, 无法保证线程安全

public class Main {
    public static void main(String[] args) {
        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                LazyLyra lyra = LazyLyra.getLyra();

                System.out.println(lyra + "-----------" + Thread.currentThread().getName());
            }).start();
        }

    }
}

class LazyLyra {
    private static LazyLyra lyra = null;

    public static LazyLyra getLyra() {
        if (lyra == null) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            lyra = new LazyLyra();
        }
        return lyra;
    }
}

image

懒汉式0.3

引入synchronized 关键字, 使线程进行同步操作, 这样做虽然保证了线程安全, 但在一定程度上也削弱了性能,
由于进入方法前获取锁操作时会消耗一定时间, 从而对性能进行影响.

public class Main {
    public static void main(String[] args) {
        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                LazyLyra lyra = LazyLyra.getLyra();

                System.out.println(lyra + "-----------" + Thread.currentThread().getName());
            }).start();
        }

    }
}

class LazyLyra {
    private static LazyLyra lyra = null;

    public synchronized static LazyLyra getLyra() {
        if (lyra == null) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            lyra = new LazyLyra();
        }
        return lyra;
    }
}

image

懒汉式0.3

synchronized改为 synchronized 语句块对成员变量进行判断 若成员变量未被初始化时 才进行加锁操作 这样做的好处是仅仅只在初始化是才会进行加锁操作, 优化了性能
在进行变量初始化是 在JVM会被分为三步:

  1. 分配内存空间
  2. 初始化对象
  3. 将初始化好的对象链接到分配好的内存空间中
    JVM在执行时会进行重排序, 由于1 3 步执行的花费时间较少, JVM会优先执行1 3 步 在然后执行2
    这样就会出现一个问题: 执行1, 2 时并未初始化好对象, 这时候已经将未初始化好的对象指向到内存空间中, 从而使成员变量不为空, 从而不会进行if判断 直接将未初始化完毕的对象进行返回
    要解决这个问题, 就要在成员变量中添加volatile关键字, 使JVM按123的顺序进行执行.
public class Main {
    public static void main(String[] args) {
        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                LazyLyra lyra = LazyLyra.getLyra();

                System.out.println(lyra + "-----------" + Thread.currentThread().getName());
            }).start();
        }

    }
}

class LazyLyra {
    private volatile static LazyLyra lyra = null;

    public static LazyLyra getLyra() {
        if (lyra == null) {
            synchronized (LazyLyra.class) {
                if (lyra == null) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    lyra = new LazyLyra();
                }
            }
        }

        return lyra;
    }
}

image

静态内部类

这种方式使用的较多且是懒汉式 加载Lyra类时并不会加载静态内部类 当调用方法时才会进行加载静态内部类, 所以是懒汉式的.

public class Main {
    public static void main(String[] args) {
        Lyra lyra = Lyra.getInstance();
        Lyra instance = Lyra.getInstance();

        System.out.println(lyra + "==========" + instance);
    }
}

class Lyra {
    private Lyra() {}

    private static class LyraInstanceClass {
        private static Lyra lyra = new Lyra();
    }

    public static Lyra getInstance() {
        return LyraInstanceClass.lyra;
    }
}

image

枚举

通过使用枚举来对实例进行初始化, 这种方式是线程安全的. enum可以避免反序列化对单例进行破坏操作.

public class Main {
    public static void main(String[] args) {
        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                System.out.println(LyraEnum.HEARTSTRINGS.getName().hashCode());
            }).start();
        }

    }
}


enum LyraEnum {
    HEARTSTRINGS("lyra");
    private String name;

    LyraEnum(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

image

posted @   RainbowMagic  阅读(26)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示