多线程笔记 - 单例

单例创建实例, 网上有很多的例子, 我这里也只是做一下笔记. 可能并不比别人的详细. 主要是为了自己做点云笔记.

1. 饿汉式

public class Ehan  {
    //1. 提供一个静态实例
    private final static Ehan instance = new Ehan();

    //2. 私有化构造函数
    private Ehan(){}

    //提供一个对外获取实例的方法
    public  static Ehan getInstance(){        return instance;
    }
}

测试:

public static void main(String[] args) {
    final CyclicBarrier barrier = new CyclicBarrier(100);
    for (int i = 0; i < 100; i++) {
        new Thread(() -> {
            try {
                barrier.await();
                System.out.println(Thread.currentThread().getName() + " 被唤醒。。。");
                Ehan obj = Ehan.getInstance();
                System.out.println(obj.hashCode());
            }
            catch (Exception e) {
                e.printStackTrace();
            }
        }, "thread" + i).start();
    }

    System.out.println("当前线程数 : " + barrier.getParties());
}

 

结果:

 所有的 hashcode 都是一样的, 说明是同一个实例.

优点: 线程安全的

缺点: 类加载的时候, 就完成实例化了(如使用了该类的其他静态属性或静态方法, 就会完成实例化, 但事实上, 我可能并不需要他实例化). 如果后面我并不使用这个类, 那不是浪费了么.

 

2. 懒汉式

public class LanHan {
    //1. 定义一个静态变量
    private static LanHan instance;

    //2. 私有化构造函数
    private LanHan(){}

    //3. 提供一个对外获取实例的方法
    public synchronized static LanHan getInstance(){
        if(instance == null){            
            instance = new LanHan();
        }
        return  instance;
    }
}

getInstance() 上的 synchronized 不能省, 省了可能会出现问题.

测试方法仍然用上面的测试方法, 只是把类改一下就行了.

  对 getInstance() 方法进行修改, 干掉 synchronized , 并在创建前面加个休眠, 模拟干点别的操作, 耗费了点时间.

public static LanHan getInstance(){
    if(instance == null){
        try {
            Thread.sleep(500);
        }
        catch (InterruptedException e) {
            e.printStackTrace();
        }
        instance = new LanHan();
    }
    return  instance;
}

结果: 

 

那这里出现了不同结果, 发现并没有创建单例, 而是创建了不同的实例出来了.

这是由于方法上没有加锁, 不同线程都能进来, 然后很多线程在休眠哪里停住了, 后面的线程追上来, 也在休眠.(这里其实造成了线程堆积)

当休眠完成后, 继续执行代码, 就创建实例了.

懒模式的优缺点:

优点:

1. 克服了饿模式的加载创建问题, 当调用 getInstance() 方法的时候, 才会去创建实例. 相当于是按需加载.

2. 线程安全.

缺点:

1. 每次调用 getInstance() 时, 都会进行 为空判断. 

2. getInstance() 方法加了锁, 并发调用时, 需要排队, 当一个线程释放锁后, 其他线程需要对锁竞争.

 

3.  双重判断

/**
 * double checked locking
 **/
public class DCL {

    //1. 提供一个静态引用
    private static volatile DCL instance;

    //2. 私有化构造函数
    private DCL(){}

    //3. 提供一个对外的获取实例接口
    public static DCL getInstance(){
        if(instance == null){
            synchronized (DCL.class){
                if(instance == null){
                    instance = new DCL();
                }
            }
        }
        return instance;
    }
}

结果:

 

 在锁里面再判断一次, 保证了线程安全.

 同样的, 对这里的 getInstance() 方法进行一个小修改:

public static DCL getInstance() {
    if (instance == null) {
        try {
            Thread.sleep(500);
        }
        catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (DCL.class) {
            // if(instance == null){
            instance = new DCL();
            // }
        }
    }
    return instance;
}

这里, 我将 锁里面的判断注释掉, 并在锁外面加个睡眠, 运行起来看一下:

 

同样的道理,

第1条线程进来时, 为空√, 进入休眠

第2条线程进来时, 为空√, 进入休眠. 第1条线程可能还在休眠, 更不可能把实例创建出来了

......

一些逻辑处理, 可能需要时间, 创建一个实例的时候, 可能并不仅仅是要new出来, 可能之前要做一些逻辑处理, 会消耗一点时间, 跟这里的睡眠效果比较像.

所以要在锁里面, 再加一个为空判断. 因为锁里面的代码, 只能一个线程进去执行, 所以即使再进行别的逻辑处理, 耗费时间, 也不会出现以上情况. \

直到这条线程创建完毕之后, 别的线程再进来时, 就能判断到实例已创建.

 

4. 内部静态类方式

public class Inner {
    //1. 私有化构造函数
    private Inner(){}

    //2. 静态类
    private static class InnerBuilder{
        private final static Inner instance = new Inner();
    }

    //3. 提供一个获取实例的方法
    public static Inner getInstance(){
        return InnerBuilder.instance;
    }
}

私有化构造函数这步, 在所有的方法中, 都是不能省的, 不然可以通过new的方式来创建, 就不是单例了.

结果:

这种方式是推荐使用的方式.

优点: 

1. 线程安全

2. 代码简单

3. 不用加锁 (有一个初始化锁, 但不是人为加入的)

4. 延迟加载(内部类只有被外部类加载时, 才会进行加载) 

 

5. 枚举类

public enum EnumObj {
    INSTANCE;
}

枚举被认为是常量。

也有类的功能, 里面可以写方法, 属性。

 

一般情况下, 使用内部静态类的方式就行了.

posted @ 2020-02-24 18:04  Sniper_ZL  阅读(189)  评论(0编辑  收藏  举报