目录
保证一个类仅有一个实例,并提供一个全局访问点。
类型:创建型
使用场景:想确保任何情况下都绝对只有一个实例
优点:在内存里只有一个实例,减少了内存的开销。可以避免对资源的多重占用。设置全局访问点,严格控制访问。
缺点:没有接口,扩展困难,想扩展只能修改代码。
重点:私有构造器(禁止从单例模式外部调用构造函数),线程安全,延迟加载,序列化和反序列化安全,反射
普通懒汉式
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton(){
}
public static LazySingleton getInstance(){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
关于懒汉式,顾名思义是延迟加载。这种模式对于单线程来说没有任何问题,但是对于多线程的话,就可能会出问题。
假如代码中线程1执行到了第8行,线程2来到了第7行,线程1这是还没有new对象,线程2中的if自然是true,这样两个线程都会new对象,就破坏了单例。
public class T implements Runnable {
@Override
public void run() {
LazySingleton lazySingleton = LazySingleton.getInstance();
System.out.println(Thread.currentThread().getName() + " " + lazySingleton);
}
}
public class Test {
public static void main(String[] args) {
Thread t1 = new Thread(new T());
Thread t2 = new Thread(new T());
t1.start();
t2.start();
System.out.println("program end;");
}
}
通过线程debug,当线程1到这一行的时候
线程2也进来了
此时由于线程1还没有new,所以线程2往下走可以进if中,再回到线程1往下走
对象hash为538,线程2往下走
变成了539,放开断点
虽然运行结果都一样,但是通过debug我们可以看出,实际上是产生了两个对象,只不过后产生的覆盖了前面产生的而已。要解决这样的问题有几种办法:
1.加synchronized,顺嘴提一句,synchronized修饰static方法,锁的是这个class文件本身,修饰普通方法,锁的是栈中的对象
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton(){
}
public synchronized static LazySingleton getInstance(){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
如此,当线程1在方法中的时候,线程2是阻塞状态,没有办法进方法的。当然由于同步锁的方式,有上锁和解锁的步骤,所以对内存开销比较大。于是演进出了下面一种方式 :
DoubleCheck双重检查
public class LazyDoubleCheckSingleton {
private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
private LazyDoubleCheckSingleton(){
}
public static LazyDoubleCheckSingleton getInstance(){
if(lazyDoubleCheckSingleton == null){
synchronized (LazyDoubleCheckSingleton.class){
if(lazyDoubleCheckSingleton == null){
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazyDoubleCheckSingleton;
}
}
将synchronized缩小到方法体中,这样,就减少了内存开销,但这样也是有隐患的,在内存初始化的时候会有几个步骤:
1.分配内存给对象
2.初始化对象
3.设置instance指向刚分配的内存地址
4.访问对象
在单线程中,为了使性能更好,2和3是可以互换位置的(重排序)。但在多线程中,线程1中假如2和3互换了位置,在线程1设置instance指向内存空间后(此时还没有初始化),线程2进来开始判断instance是否是null,由于此时已经分配了内存空间,所以不为null,所以线程2开始访问对象,但是因为线程1还没有开始初始化对象,所以线程2访问的是有问题的。
所以为了解决这个问题,我们只需要加volatile关键词,这样重排序就会禁止了。
当然,我在线程1的创建过程中,不让线程2看到细节,线程2就不能访问了,于是有了下面的版本
静态内部类
public class StaticInnerClassSingleton {
private static class InnerClass{
private static StaticInnerClassSingleton staticInnerClassSingleton;
}
public static StaticInnerClassSingleton getInstance(){
return InnerClass.staticInnerClassSingleton;
}
private StaticInnerClassSingleton(){
}
}
JVM在类被初始化阶段(class被加载后,被线程使用之前),jvm会获取一个锁,用来同步多个线程对一个类的初始化。
类会在这几种情况下被初始化:
1.该类的实例被创建
2.类中声明的静态方法被调用
3.类中声明的静态成员被赋值
4.类中声明的静态成员被使用,且这个成员不是常量成员
5.该类是顶级类,且有嵌套的断言语句
所以关于静态内部类,就是使用了jvm的特性,只有一个线程可以拿到class的初始化锁,拿到初始化锁的就去获取对象。
饿汉式
public class HungrySingleton {
private final static HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
这种写法很简单,在类加载的时候就初始化好了,这样也就避免了线程同步的问题,当然如果用不到,会造成资源的浪费。还可以写在静态代码块中:
public class HungrySingleton {
private final static HungrySingleton hungrySingleton = new StaticInnerClassSingleton();
static {
hungrySingleton = new HungrySingleton();
}
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}