单利模式是经常使用到的设计模式,例如spring的默认bean为单例。
同时众多框架体系使用的为多线程体系,所以在使用的时候单例和多线程结合起来可能会有一些没有注意到的事情,例如全局变量的控制等。

单例模式问题

单例核心就是构造方法私有化,使用static类方法返回对象的唯一实例。

立即加载/饿汉模式=>属性覆盖问题

  • 代码
//立即加载,饿汉
class AA{
    private static AA aa=new AA();
    private AA(){
    }
//方法未同步生成多线程情况下的实例对象属性只能为同一个
    public static AA getInstance(){
//也可以根据不同的需求,同一对象生成不同的属性实例,如果该单例有多个不同属性,且对不同线程要求不同的话,变量的属性会被覆盖
        return aa;
    }
    public static void main(String[] args) {
        AA instance = AA.getInstance();
        new Thread(()-> System.out.println(instance.hashCode())).start();
        new Thread(()-> System.out.println(instance.hashCode())).start();
        new Thread(()-> System.out.println(instance.hashCode())).start();
    }
}

image.png

  • 问题
    如果单例对象被多个线程使用,但是变量里面的属性被多个线程操作会出现线程不安全问题。即属性会被覆盖

延迟加载/懒汉模式=>违背单例原则问题

使用懒汉模式可以改变上述的属性覆盖问题,但是多个线程情况下

//延迟加载,懒汉
class AB {
    private static AB ab;
    private AB() {
    }
    public static AB getInstance() {
        if (ab == null) {
       //线程休眠保障有足够的时间判断ab,模拟多个线程同时进入的情况
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ab = new AB();
        }
        return ab;
    }
    public static void main(String[] args) {
        new Thread(() -> System.out.println(AB.getInstance().hashCode())).start();
        new Thread(() -> System.out.println(AB.getInstance().hashCode())).start();
        new Thread(() -> System.out.println(AB.getInstance().hashCode())).start();
    }
}

如下图,如果多个线程同时生成,可能会同时返回多个实例对象,虽然全局属性ab只会指向同一个AB,但是生成了多个对象实例,且可以被其他变量重新引用,违背了单例原则
image.png

单例模式问题解决

懒汉模式问题synchronized解决

  • 使用synchronized锁方法
    加入static getInstance方法,锁上单例class类,但是这会造成线程效率低下
    public  synchronized static AB getInstance() {...}
  • 使用synchronized锁代码块
 public static AB getInstance() {
//synchronized (ab)如果ab=null则会NPE,锁class
        synchronized (AB.class) {
            if (ab == null) {
                //线程休眠保障有足够的时间判断ab,模拟多个线程同时进入的情况
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ab = new AB();
            }
            return ab;
        }

锁上ab对象,但是还是很慢,synchronized锁方法一样

其他地方没法上锁,因为判断ab==null是唯一条件,锁其他地方没有意义

懒汉模式问题volatile和DCL机制解决

通过DCL(双重检查锁),在new对象之前在加锁检查是否有对象已经生成
通过volatile禁止重排序和增加可见性,直接拉取全局变量到线程私有栈中查看比较

//延迟加载,懒汉解决办法
class AC {
    private static AC ac;
    private AC() {
    }
    public static AC getInstance() {
        if (ac == null) {
            //线程休眠保障有足够的时间判断ab,模拟多个线程同时进入的情况
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (AC.class) {
                if (ac == null) {
                    ac = new AC();
                }
            }
        }
        return ac;
    }
    public static void main(String[] args) {
        new Thread(() -> System.out.println(AC.getInstance().hashCode())).start();
        new Thread(() -> System.out.println(AC.getInstance().hashCode())).start();
        new Thread(() -> System.out.println(AC.getInstance().hashCode())).start();
    }
}

内存的地址一样
image.png

静态内部类解决单例

class AD{
    private static class StaticInnerClass{
        private  static AD ad=new AD();
    }
    private AD(){
    }
    private static AD getInstance(){
        return StaticInnerClass.ad;
    }
}

序列化与反序列化的单例模式

有些时候,例如远程调用和数据库存储等需要实现序列化,不然无法转换为ObjectOutputStream

问题

序列化之后再进行反序列化多个时会生成多例

  • static变量不属于实例,是不会被序列化共享的,其值不会被记录(即不太清楚了。。。)
  • transient关键字声明的也不会被实例化
//序列化与反序列化多例
class BA implements Serializable {
    public static class UserInfo {
    }
    private static final long serialVersionUID = -1L;
    public static UserInfo userInfo = new UserInfo();
    private static BA ba = new BA();
    private BA() {
    }
    public static BA getInstance() {
        return ba;
    }
    public static void main(String[] args)  {
        BA ba = BA.getInstance();
        System.out.println("序列化实例ba=" + ba.hashCode() + "userInfo=" + BA.userInfo.hashCode());
        try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(new File("BA.txt")))) {
           objectOutputStream.writeObject(ba);
        } catch (IOException e) {
            e.printStackTrace();
        }
        try (ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(new File("BA.txt")))) {
            BA ba1 = (BA)objectInputStream.readObject();
            System.out.println("反序列化实例ba=" + ba1.hashCode() + "userInfo=" + BA.userInfo.hashCode());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

userInfo地址相同,因为其属于class
ba地址不同,其生成了新的实例
image.png

解决

ObjectInputStream在进行反序列化的时候,会检查反序列化的类有没有定义readResolve方法()如果有定义则调用里面的方法,不创建新的对象
image.png
所以新增一个readResolve方法返回实例即可

class BB implements Serializable {
    public static class UserInfo {
    }

    private static final long serialVersionUID = -1L;
    public static UserInfo userInfo = new UserInfo();
    private static BB bb;
    static {
        bb=new BB();
    }

    private BB() {
    }

    public static BB getInstance() {
        return bb;
    }
    private Object readResolve(){
        return BB.getInstance();
    }
    public static void main(String[] args) {
        BB bb = BB.getInstance();
        System.out.println("序列化实例bb=" + bb.hashCode() + "userInfo=" + BB.userInfo.hashCode());
        try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("BB.txt"))) {
            objectOutputStream.writeObject(bb);
        } catch (IOException e) {
            e.printStackTrace();
        }
        try (ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("BB.txt"))) {
            BB bb1 = (BB) objectInputStream.readObject();
            System.out.println("反序列化实例bb=" + bb1.hashCode() + "userInfo=" + BB.userInfo.hashCode());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

hashcode相同
image.png

使用static代码块实现单例

在类加载的时候生成一次实例,缺点是多线程情况下依然会有属性共享问题

class AE {
    private static AE ae;
    static {
        ae = new AE();
    }
    private AE() {
    }
    public static AE getInstance() {
        return ae;
    }
}

枚举enum单例

http://jtao.work/archives/di-wu-zhang--mei-ju
枚举是实现单例的最佳选择,将需要的类定义为枚举类即可。


这个家伙很懒,啥也没有留下😋