有些对象我们只需要一个如线程池、缓存dataSource、硬件设备等。如果有多个实例会造成相互冲突、结果不一致的问题,毕竟你有我也有,但是你有的和我有的不一定真的一模一样,是同一个。 使用单例模式可以确保一个类最多只有一个实例,并提供一个全局的访问点。
public class Test { public class ABC { public ABC() { } // private ABC() { //为这个内部类申明私有的构造方法,外部则无法通过new初始化,只能类自己初始化 // } // ABC n1 = new ABC(); } public class CDB { public CDB() { ABC n1, n2; n1 = new ABC(); n2 = new ABC(); System.out.println("CBD: " + (n1 == n2)); //false } } public static void main(String[] args) { ABC n1, n2; n1 = new Test().new ABC(); n2 = new Test().new ABC(); System.out.println("main: " + (n1 == n2)); //false new Test().new CDB(); } } 复制代码
那么有什么方法可以使得每次new出来的对象都是同一个呢,看看下面单例模式类图,就可以找到一些思路了!
Singleton(单例) | |
---|---|
static uniqueInstance(静态的唯一对象申明) | |
private singleton() (私有的实例化方法) | |
static getInstance() (全局访问点) |
了解了上面的内容,我们来写一个简单的单例模式代码,代码如下:
public class Singleton { private static Singleton uniqeInstance = null; //静态变量 private Singleton() { // 私有的构造方法,外部无法使用 } public static Singleton getInstance() { if (uniqeInstance == null) { uniqeInstance = new Singleton(); } return uniqeInstance; } } 复制代码
静态变量由于不属于任何实例对象,是属于类的,所以在内存中只会有一份,在类的加载过程中,JVM为静态变量分配一次内存空间。
===> Java之static静态关键字详解
这个场景我们想象一下:一个食品工厂,工厂只有一个,然后工厂里也只有一个锅,制作完一批食品才能制作下一批,这个时候我们的食品工厂对象就是单例的了,下面就是模拟实现的代码,代码的单例实现和上面的简单实现不同,做了优化处理,稍后会解释为什么要优化
public class ChocolateFactory { private boolean empty; // 空锅 private boolean boiled; // 加热 public volatile static ChocolateFactory uniqueInstance = null; private ChocolateFactory() { empty = true; // 锅是空的 boiled = false; // 还没加热 } public static ChocolateFactory getInstance() { if (uniqueInstance == null) { synchronized (ChocolateFactory.class) { if (uniqueInstance == null) { uniqueInstance = new ChocolateFactory(); } } } return uniqueInstance; } // 第一步装填 public void fill() { if (empty) { // 锅是空的 // 添加原料巧克力动作 empty = false; // 锅装满了,不是空的 boiled = false; // 还没加热 } } // 第三步倒出 public void drain() { if ((!empty) && boiled) { // 锅不是空的,已经加热 // 排出巧克力动作 empty = true; //出锅,锅空了 } } // 第二步加热 public void boil() { if ((!empty) && (!boiled)) { // 锅不是空的,没加热 // 煮沸 boiled = true; // 已经加热 } } } 复制代码
在多线程的情况下,会有时间片的概念,cpu竞争,这刚好就是单例模式可能会发生问题的时候,会发生什么样的问题呢?以食品加工厂代码为例
public synchronized static ChocolateFactory getInstance() { if (uniqueInstance == null) { uniqueInstance = new ChocolateFactory(); } return uniqueInstance; } 复制代码
在多线程情况下会实例化出两个对象
线程1执行到 if (uniqueInstance == null)
,被线程2抢走了执行权,此时线程1还没有new对象;线程2同样来到 if (uniqueInstance == null)
,发现没有对象实例,也打算实例化对象;最后线程1线程2都会执行 uniqueInstance = new ChocolateFactory();
此时可以在
getInstance() 方法前加上
synchronized
修饰符同步方法,但是在多线程调用比较频繁的时候,这种方式比较耗费性能。
public class ChocolateFactory { public static ChocolateFactory uniqueInstance = new ChocolateFactory(); //“急切”创建实例 public static ChocolateFactory getInstance() { if (uniqueInstance == null) { uniqueInstance = new ChocolateFactory(); } return uniqueInstance; } } 复制代码
public static ChocolateFactory uniqueInstance = new ChocolateFactory();
在应用启动的时候就加载初始化一次实例对象,这个时候多线程调用永远也只会有一个实例,因为 if (uniqueInstance == null)
的结果一直是false;但如果这对单例对象在应用中没有地方用到,使用这种方式则耗费掉了一些内存空间
public class ChocolateFactory { //用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最的值。 public volatile static ChocolateFactory uniqueInstance = null; public static ChocolateFactory getInstance() { if (uniqueInstance == null) { synchronized (ChocolateFactory.class) { if (uniqueInstance == null) { uniqueInstance = new ChocolateFactory(); } } } return uniqueInstance; } } 复制代码
首先 public volatile static ChocolateFactory uniqueInstance = null;
没有在应用启动的时候就初始化对象,节省了内存;其次 synchronized
修饰的代码块是再 if (uniqueInstance == null) {}
判断里面的,只有符合条件才会进入同步方法,减少了性能消耗。