多线程笔记 - 单例
单例创建实例, 网上有很多的例子, 我这里也只是做一下笔记. 可能并不比别人的详细. 主要是为了自己做点云笔记.
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; }
枚举被认为是常量。
也有类的功能, 里面可以写方法, 属性。
一般情况下, 使用内部静态类的方式就行了.