前言:什么叫设计模式?
设计是解决软件开发某些特定问题而提出的一些解决方案也可以理解成解决问题的一些思路。通过设计模式可以帮助我们增强代码的可重用性、可扩充性、 可维护性、灵活性好。我们使用设计模式最终的目的是实现代码的高内聚和低耦合。
单例模式
一、定义
确保一个类只有一个实例,并提供了一个全局访问点来访问该实例。
二、注意点
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
三、类型
饿汉式:
在类加载时默认创建一个实例,之后调用getInstance()默认返回该已创建实例。
优点:线程安全。
缺点:过早浪费资源。
public class Singleton {
private static final Singleton singleton = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return singleton;
}
};
特征:
1、构造函数用private修饰,外部无法访问;
2、声明静态对象时就初始化;
3、static关键字修饰,静态变量,存储在内存中,只有一份数据;
4、final关键字,只初始化一次,所以只有一个singleton实例。
懒汉式:
当程序启动之后并不会进行初始化,在什么时候调用什么时候初始化。
优点:使用时才创建,节约资源。
缺点:线程不安全。
public class Singleton {
//没有经过初始化默认为空
private static Singleton singleton;
private Singleton(){
}
public static synchronized Singleton getInstance(){
if(singleton==null){
singleton = new Singleton();
}
return singleton;
}
};
特征:
1、构造函数用private修饰,外部无法访问;
2、使用时(调用getInstance方法)才初始化;
3、static关键字修饰,静态变量,存储在内存中,只有一份数据;
4、线程不安全,需要配合synchronized同步锁保证多线程情况下的单例唯一性。
四、线程安全问题解析
线程不安全的原因?
1、线程是抢占执行的。
2、有的操作不是原子的。当 cpu 执行一个线程过程时,调度器可能调走CPU,去执行另一个线程,此线程的操作可能还没有结束;(通过锁来解决)
3、多个线程尝试修改同一个变量(一对一修改、多对一读取、多对不同变量修改,是安全的)
4、内存可变性。
5、指令重排序:java的编译器在编译代码时,会针对指令进行优化,调整指令的先后顺序,保证原有逻辑不变的情况下,来提高程序的运行效率。
分析:
1、对于饿汉模式来说,多线程同时调用getInstance(),由于getInstance()里只做了一件事:读取instance实例的地址,也就是多个线程在同时读取同一个变量,并没有构成多个线程同时修改同一个变量这一情况,所以说饿汉模式是线程安全的。
2、而对于懒汉模式来说,多线程调用getInstance(),getInstance()做了四件事情:
(1)读取 singleton的内容
(2)判断 singleton是否为 null
(3)如果 singleton为null,就 new实例 (这就会修改 singleton的值)
(4)返回实例的地址
由于懒汉模式造成了多个线程同时修改同一个变量这一情况,所以说懒汉模式是线程不安全的。
五、线程安全问题解决方法
方法一:加锁
//懒汉模式
public class Singleton {
private Singleton() {
}
private static Singleton instance = null;
public static Singleton getInstance() {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
return instance;
}
然而,在上面的代码中,即使instance已经实例化了,但是每次调用getInstance()还是会涉及加锁解锁,实际上此时已经不需要了,所以要实现在instance实例化之前调用的时候加锁,之后不加锁,就引出了双重检验锁版本。
方法二:双重检验锁(升级版)
//懒汉模式
public class Singleton {
private Singleton() {
}
private static Singleton instance = null;
public static Singleton getInstance() {
//双重检验锁
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
到这里我们已经基本完成懒汉模式的设计,但是懒汉模式在多线程情况下由于编译器优化还会出现一种特殊情况,某个线程可能会进行多次读操作:
线程1第一次读取instance为空,而线程2未释放锁时,加锁失败,与此同时其他线程也读取到instance为空,于是实例化instance后线程2释放锁,此时线程1加锁成功,但读取instance仍然为null。此时的线程2进行了多次读操作,然而由于编译器优化,导致线程2没有读到最新的数据,即实例化的instance。
此时需要使用volatile来解决这种特殊情况带来的问题。
方法三:双重检验锁+volatile
在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的。
Java提供了volatile来保证可见性,当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取。
//懒汉模式
public class Singleton {
private Singleton() {
}
//volatile 避免内存可见性引出的问题
private volatile static Singleton instance = null;
public static Singleton getInstance() {
//双重检验锁
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
总结:
为了保证懒汉模式线程安全,涉及到三个要点:
1、加锁保证线程安全
2、双重if保证效率
3、volatile避免内存可见性引来的问题.