单例模式 : 主要作用是保证在整个Java项目中, 该类只有一个实例对象存在; 单例模式的实现方式有多种, 主要以饿汉式加载和懒汉式加载进行举例;
一, 饿汉式加载
饿汉式加载是在类加载器对该类进行加载的时候就进行对象初始化, 对象在初始化的时候, 访问线程还没有开启, 因此不存在线程问题; 但是饿汉式加载无论该对象在项目运行时是否会被使用, 都会进行对象初始化, 因此容易造成内存浪费;
单例类 :
public class HungrySingle { private static HungrySingle hungrySingle = new HungrySingle(); private HungrySingle() {} public static HungrySingle createInstance() { return hungrySingle; } }
测试类 :
public static void main(String[] args) { CountDownLatch countDownLatch = new CountDownLatch(20); for(int i = 0; i < 20; i++) { new Thread(new Runnable() { @Override public void run() { try { countDownLatch.await(); HungrySingle single = HungrySingle.createInstance(); System.out.println(single); } catch (Exception e) { e.printStackTrace(); } } }, "线程 : " + i).start(); countDownLatch.countDown(); } }
测试结果 :
二, 懒加载式 :
懒加载式的单例模式, 是在第一次调用该类的时候, 进行该类对象的初始化过程; 在初始化方法中, 需要对当前对象进行判断, 如果已经初始化过, 直接返回初始化对象, 如果没有进行初始化, 进行对象初始化; 因为在初始化过程中, 存在对象初始化的分支判断, 所以懒加载方式的单例模式存在线程安全问题, 在高并发条件下不能保证对象实例的绝对单一化
1, 懒加载 ---- 线程不安全
* 实例创建类
public class LazySingle { private static LazySingle lazySingle = null; private LazySingle() {} public static LazySingle createInstance() { if(null == lazySingle) { lazySingle = new LazySingle(); return lazySingle; } else { return lazySingle; } } }
* 测试类
public static void main(String[] args) { CountDownLatch countDownLatch = new CountDownLatch(20); for(int i = 0; i < 20; i++) { new Thread(new Runnable() { @Override public void run() { try { countDownLatch.await(); LazySingle instance = LazySingle.createInstance(); System.out.println(instance); }catch (Exception e) { e.printStackTrace(); } } }).start(); countDownLatch.countDown(); } }
* 测试结果
多运行几次后会发现, 在打印出来的实例地址中, 存在其他实例; 这是因为CPU在分配内存资源时, 有两个线程同时抢到(null == single)分支, 对象初始化后存在其他的对象地址, 下面我们来进行线程同步;
2, 懒加载 ---- 线程安全_同步方法
线程方法同步很简单, 在原方法名称上添加关键字synchronized
public synchronized static LazySingle createInstance() { if(null == lazySingle) { lazySingle = new LazySingle(); return lazySingle; } else { return lazySingle; } }
进行多次运行后, 单例模式运行正常, 看起来线程安全问题得到解决;
但是, 线程方法同步在高并发情况下存在一定的性能问题; 添加同步关键字后, 并发情况下对象创建, 会进行队列处理, 前一个线程释放同步锁后, 后一个对象才能拿到该锁进行线程访问; 这样避免了在非线程安全情况下同时抢到空对象的可能, 但是如果对象创建是一个比较复杂的过程, 容易让后面的线程产生长时间的等待, 体验相对较差; 这样可以使用同步代码块进行避免, 直接用同步代码块的双重判空进行处理;
3, 懒加载 ---- 线程安全_同步代码块双重判空处理
public static LazySingle createInstance() { if(null == lazySingle) { synchronized (LazySingle.class) { if(null == lazySingle) { lazySingle = new LazySingle(); } } } return lazySingle; }
这种方式首先避免了线程不安全问题, 不可能同时存在多个线程抢到第二层的非空校验; 同时也解决了同步方法产生的性能问题, 通过第一次校验, 能同时抢到(null == single)分支的线程本就极少, 这样需要排队等待的线程相对只有有限的几道线程, 对整个流程影响几乎可以忽略不计
3, 懒加载 ---- 线程安全_静态内部类实现
静态内部类是在当所属的类第一次被引用时进行加载, 并且只加载一次, 这样避免了饿汉式加载造成的内存浪费和同步代码块/同步方法造成的一定程度上的线程等待;
public class StaticInnerSingle { private StaticInnerSingle() {} public static StaticInnerSingle createInstance() { return InnerSingle.single; } static class InnerSingle { public static final StaticInnerSingle single = new StaticInnerSingle(); } }