单例模式

单例模式

饿汉式

public class SingleTonCrazy {
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            Thread t = new MyThread();
            t.start();
        }
    }
}
class SingleTon {
    // 先将构造方法私有化,外部不能定义
    private SingleTon() {}
    // 成员变量
    private static final SingleTon instance = new SingleTon();

    public static SingleTon getInstance() {
        return instance;
    }
}
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(SingleTon.getInstance().hashCode());
    }
}

创建了100个线程,来测试打印出来的hashcode,结果是一样的。

image-20230402130932820

懒汉式

饿汉式单例模式存在一个浪费空间的问题,如果这个对象一直不使用,那么就会一直在堆中存放。

下面的懒汉式就应运而生了

public class SingleTonCrazy {
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            Thread t = new MyThread();
            t.start();
        }
    }
}

class SingleTon {
    // 懒汉式
    private SingleTon(){};

    private static SingleTon instance = null;

    public static SingleTon getInstance() {
        if (instance == null) {
            instance = new SingleTon();
        }
        return instance;
    }
}


class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(SingleTon.getInstance().hashCode());
    }
}

创建了一百个线程,打印对象的hashcode值,发现有不一样的,说明这种懒汉式存在线程安全问题,有可能会有多个线程在同一时间进入instacne == null这个判断逻辑中,那么不同的线程就会创建不同的对象。

image-20230402132143098

改进后的懒汉式

怎么解决呢?加锁

public class SingleTonCrazy {
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            Thread t = new MyThread();
            t.start();
        }
    }
}
class SingleTon {
    // 懒汉式
    private SingleTon(){};

    private static SingleTon instance = null;

    public static synchronized SingleTon getInstance() {
        if (instance == null) {
            instance = new SingleTon();
        }
        return instance;
    }
}
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(SingleTon.getInstance().hashCode());
    }
}

给getInstance()这个方法加了一把锁,所有的线程来都是串行化的来,所以最后得到的结果肯定都是一样的,但是这样的效率比较低,每一个线程创建单例对象都要等待锁的释放。

image-20230402132517899

减小锁粒度

给整个方法加锁的话,粒度会很大,考虑减小锁粒度

public class SingleTonCrazy {
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            Thread t = new MyThread();
            t.start();
        }
    }
}
class SingleTon {
    // 懒汉式
    private SingleTon(){};

    private static SingleTon instance = null;

    public static  SingleTon getInstance() {
        /**
         * 在这可以执行其他逻辑
         */
        synchronized (SingleTon.class) {
            if (instance == null) {
                instance = new SingleTon();
            }
            return instance;
        }
    }
}
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(SingleTon.getInstance().hashCode());
    }
}

最后得到的结果,hashcode的值都是一样的

image-20230402132838905

再次改进的懒汉式(双重校验锁Double Check Lock)

Double Check Lock(DCL)

在下边这个代码里边,就算已经有一个线程创建了单例,那么其他的线程还是要依次拿到锁,进入synchronized代码内部,拿到单例,这样损耗性能。

public static  SingleTon getInstance() {
    /**
     * 在这可以执行其他逻辑
     */
    synchronized (SingleTon.class) {
        if (instance == null) {
            instance = new SingleTon();
        }
        return instance;
    }
}

考虑在synchronized外部再判断依次为不为null,如果不是空了直接返回instance,为空的话,放一个线程进来,创建单例,这样在单例创建好之后,多线程可以并发的拿到对象

public class SingleTonCrazy {
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            Thread t = new MyThread();
            t.start();
        }
    }
}
class SingleTon {
    // 懒汉式
    private SingleTon(){};

    private static SingleTon instance = null;

    public static  SingleTon getInstance() {
        /**
         * 在这可以执行其他逻辑
         */
        if (instance == null) {
            synchronized (SingleTon.class) {
                if (instance == null) {
                    instance = new SingleTon();
                }
            }
        }
        return instance;
    }
}
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(SingleTon.getInstance().hashCode());
    }
}

测试结果如下

image-20230402133555054

指令重排问题

public class Test {
    public static void main(String[] args) {
        Test t = new Test();
    }
}

创建对象时候的指令

image-20230402133918476

  • new 等同于为对象分配空间(Java出于安全考虑会给成员变量赋上默认值,不会有遗留变量的问题)
  • invokespecial初始化方法,会为成员变量赋上初始值
  • astore_1会将变量和实际地址建立联系

如果发生了指令重排,执行顺序变成以下:

  1. new
  2. astore_1
  3. invokespecial

也就是说先建立了联系,那么变量中就有了对象的实际地址,就不为空了,再上述代码中,就会直接返回没有初始化的对象。

所以最终得到的单例可能是不一样的。

所以要解决指令重排问题volatile强制不指令重排。

class SingleTon {
    // 懒汉式
    private SingleTon(){};

    private static volatile SingleTon instance = null;

    public static  SingleTon getInstance() {
        /**
         * 在这可以执行其他逻辑
         */
        if (instance == null) {
            synchronized (SingleTon.class) {
                if (instance == null) {
                    instance = new SingleTon();
                }
            }
        }
        return instance;
    }
}
posted @ 2023-05-05 12:30  Sstarry  阅读(8)  评论(0)    收藏  举报