说在前面
不要为了套用设计模式而使用设计模式,而是,在业务上遇到问题时,很自然地想到设计模式作为一种解决方案
单例模式
是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点,属于创建型模式
适用场景
确保任何情况下都绝对只需要一个实例的场景。例如:ServletContext、ServletConfig、ApplicationContext、DBPool都是属于单例的
常见的几种写法
- 饿汉式单例
- 懒汉式单例
- 注册式单例
- ThreadLocal单例
饿汉式单例
/**
* 饿汉式单例
* 1、构造器私有,防止被别人使用new关键字创建对象
* 2、提供一个对外方法,供外部调用
*
* @author wcj
* @description
* @date 2019/8/16 17:37
*/
public class HungryMan {
/**
* 第一种写法:静态成员创建该类实例
* 写成final是为了防止被反射获取后,进行变量的覆盖
*/
private static final HungryMan HUNGRY_MAN = new HungryMan();
/**
* 第二种写法:在静态块中创建该类实例
*/
// static {
// HUNGRY_MAN = new HungryMan();
// }
private HungryMan() {
}
/**
* 提供唯一一个对外的方法,供外部调用该类实例
*
* @return
*/
public static HungryMan getInstance() {
return HUNGRY_MAN;
}
}
在类的加载时就创建了实例。饿汉式单例优点是本身就是线程安全的。缺点是浪费了内存空间,因为这类可能不会被用到,当然在一个类这么做的时候,浪费的是很少的,如果所有类都这么做,那么就非常严重了,所以还有一种单例模式:懒汉式单例
懒汉式单例
public class LazyMan {
private static LazyMan lazyMan = null;
private LazyMan() {
}
public static LazyMan getInstance() {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
return lazyMan;
}
}
/**
* 懒汉式单例第一次优化
*
* @author wcj
* @description
* @date 2019/8/17 10:52
*/
public class OptimizeLazyMan {
private static OptimizeLazyMan lazyMan = null;
private OptimizeLazyMan() {
}
/**
* 给方法块上添加synchronized关键字,可以防止多线程情况下拿到的不是同一对象
* 缺点:虽然在jdk1.6以后对synchronized进行了性能优化,但是还是存在一定的性能问题
* 可以使用双重检查锁稍加优化
*
* @return
*/
public synchronized static OptimizeLazyMan getInstance() {
if (lazyMan == null) {
lazyMan = new OptimizeLazyMan();
}
return lazyMan;
}
}
/**
* 懒汉式单例第二次优化
*
* @author wcj
* @description
* @date 2019/8/17 10:52
*/
public class OptimizeLazyMan02 {
private static OptimizeLazyMan02 lazyMan = null;
private OptimizeLazyMan02() {
}
/**
* 因为之前的是用synchronized修饰静态方法,是类锁,效率较低
* 使用双重检查锁机制,可以稍加优化
*
* @return
*/
public static OptimizeLazyMan02 getInstance() {
if (lazyMan == null) {
synchronized (OptimizeLazyMan02.class){
if(lazyMan == null){
lazyMan = new OptimizeLazyMan02();
}
}
}
return lazyMan;
}
}
/**
* 模拟执行线程,去创建单例对象
* @author wcj
* @description
* @date 2019/8/17 10:55
*/
public class ExecuteThread implements Runnable {
@Override
public void run() {
OptimizeLazyMan02 instance = OptimizeLazyMan02.getInstance();
System.out.println(Thread.currentThread().getName() + ":" + instance);
}
}
public class SingletonTest {
public static void main(String[] args) {
//生成两个线程去访问得到对象,看是否是一致
Thread t1 = new Thread(new ExecuteThread());
Thread t2 = new Thread(new ExecuteThread());
t1.start();
t2.start();
}
}
第一次的优化,在静态方法上添加了synchronized关键字,可以有效的保证多线程的安全,缺点是修饰的是静态方法,锁的范围较大,性能较差。第二次的优化是采用了双重检查锁机制,不在静态方法上使用锁,而在方法内部。
为什么第二种优化需要两层if判断
当两个线程都同时进入第一层if判断的时候,线程A获得锁,进行new后并释放锁后,线程B进入锁后,还是会再次进行new对象,这样就会产生new对象两次,且可能造成非单例的情况产生,所以为了避免此种情况,需要再加一层if判断。
而外层的if判断是为了减少锁竞争,当对象不为空的时候,就不需要进行锁竞争,不然每个线程调用该方法时,都会进行锁的不必要竞争。下图是线程级别的debug,当线程1进入的时候,线程0就处于monitor状态了,等待线程1的释放。
延伸:synchronized修饰普通方法和静态方法的区别
- 修饰普通方法时,锁是对象锁,也就是this。当该类中有多个普通方法被Synchronized修饰(同步),那么这些方法的锁都是这个类的一个对象this。多个线程访问这些方法时,如果这些线程调用方法时使用的是同一个该类的对象,虽然他们访问不同方法,但是他们使用同一个对象来调用,那么这些方法的锁就是一样的,就是这个对象,那么会造成阻塞。
- 修饰静态方法时,锁是类锁,也就是类名.class。这个范围就比对象锁大。这里就算是不同对象,但是只要是该类的对象,就使用的是同一把锁。多个线程调用该类的同步的静态方法时,都会阻塞。
当CPU执行时,会将命令解析成JVM指令,逐步完成以下的步骤
- 分配内存给对象
- 初始化对象
- 将初始化对象和内存建立连接,也就是赋值
- 用户的初次访问
其中可能造成指令重排的问题,指令顺序每次可能都不一样,可以引用到volatile关键字用以解决指令重排的问题
静态内部单例模式
/**
* 全程没有用到synchronized关键字,性能最优
* 内部类单例模式
*
* @author wcj
* @description
* @date 2019/8/17 16:07
*/
public class InnerLazyMan {
/**
* 虽然构造方法私有了,但是反射还是可以创建的
*/
private InnerLazyMan() {
}
/**
* 当用户调用时,内部类才会执行
* 这是JVM底层的执行逻辑,完全的避免了线程安全问题
*
* @return
*/
public static final InnerLazyMan getInstance() {
return InnerMan.INNER_LAZY_MAN;
}
/**
* 这个内部类是饿汉式的,但是如果外部类没有调用getInstance(),那么内部类是不会被执行的
*/
private static class InnerMan {
private static final InnerLazyMan INNER_LAZY_MAN = new InnerLazyMan();
}
}
上图中的代码,虽然内部类是饿汉式的,但是只有外部类方法被调用才会执行,是一种特殊的懒汉式加载。但是还会有一种情形,虽然构造器被私有了,但是可以通过反射方式获得对象
public class InnerSingletonTest {
public static void main(String[] args) {
try {
//通过反射得到对象
Class<InnerLazyMan> lazyManClass = InnerLazyMan.class;
//getDeclaredConstructor()返回所有构造器,包括public的和非public的,当然也包括private的
//getConstructor()返回访问权限是public的构造器
Constructor<InnerLazyMan> constructor = lazyManClass.getDeclaredConstructor();
//值为true表示反射的对象在使用时应该取消访问权限检查,值为false表示开启
constructor.setAccessible(true);
InnerLazyMan innerLazyMan = constructor.newInstance();
InnerLazyMan instance = InnerLazyMan.getInstance();
//判断两个对象是否是同一个对象
System.out.println(innerLazyMan==instance);
}catch (Exception r){
r.printStackTrace();
}
}
}
输出的是:false,代表是这两个不是同一对象
要解决上述的情况,可以在构造器中处理,如下图所示
private InnerLazyMan() {
if(InnerMan.INNER_LAZY_MAN!=null){
throw new RuntimeException("不能创建更多的实例了");
}
}
虽然上述的功能比较完备了,不能通过反射的方式进行创建对象了,但是还有一种方式可以得到对象,那就是序列化!
通过序列化方式得到对象
/**
* 序列化式的创建单例模式,其实就是一个简单的单例模式
*
* @author wcj
* @description
* @date 2019/8/17 16:43
*/
public class SerializableLazyMan implements Serializable {
private static SerializableLazyMan serializableLazyMan = null;
private SerializableLazyMan() {
}
public static SerializableLazyMan getInstance() {
if (serializableLazyMan == null) {
serializableLazyMan = new SerializableLazyMan();
}
return serializableLazyMan;
}
}
/**
* 序列化单例测试类
* @author wcj
* @description
* @date 2019/8/17 16:43
*/
public class SerializableTest {
public static void main(String[] args) {
SerializableLazyMan s1;
SerializableLazyMan s2 = SerializableLazyMan.getInstance();
FileOutputStream fileOutputStream;
try {
//将s2写入序列化中
fileOutputStream = new FileOutputStream("1.txt");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(s2);
objectOutputStream.flush();
objectOutputStream.close();
//将对象反序列化赋给s1
FileInputStream fileInputStream = new FileInputStream("1.txt");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
s1 = (SerializableLazyMan) objectInputStream.readObject();
objectInputStream.close();
System.out.println(s1);
System.out.println(s2);
//比较两个对象是否为同一对象
System.out.println(s1 == s2);
} catch (Exception e) {
}
}
}
输出的是:false
这时我们得知,序列化可以破坏单例模式的,得到的对象并不是同一对象,那么如何避免呢?可以在单例类中添加以下代码
/**
* 添加readResolve方法
* @return
*/
private Object readResolve() {
return serializableLazyMan;
}
思考,为什么添加了这段代码,再运行测试类时,就是返回true,为同一对象呢?ObjectInputStream类中的源码如下
从上述源码可知,readObject0()中的readOrdinaryObject()方法中是判断该类是否可以被初始化,可以就new一个新的对象,那么自然输出的就是false了,被初始化后,又检查类中是否包含了readResolve()方法,包含则覆盖要被序列化类的对象,这样就可以保持对象的一致性。
重写readResolve(),只不过是覆盖了反序列化中的对象,其实对象还是创建了两次,这是发生在JVM层次中的,相对来说较为安全,之前反序列化出来的对象会被GC回收
因为序列化会破坏单例,所以还有一种单例模式是注册式单例。