目录
一、什么是单例模式?
二、单例模式的应用场景
三、两种典型的方式实现单例模式
1.饿汉模式
2.懒汉模式
3.理解懒汉模式和饿汉模式
四、单例模式和线程的关系
1.饿汉模式是否线程安全?
2.懒汉模式线程安全吗?为什么?
2.1 如何改进懒汉模式?让代码变得线程安全呢?
单例模式是一种常见的“设计模式”
某个类,不应该有多个实例,此时就可以使用单例模式(DataSource就是一个典型的案例,一一个程序中只有一个实例,不应该实例化多个DataSource对象)。如果尝试创建多个实例,编译期就会报错。
public class singlePattern {
//先创建一个表示单例的类
//我们就要求Singleton这个类只能有一个实例
//饿汉模式的单例实现
//饿汉模式的单例实现,“饿”指得是,只要类被加载,实例就会立刻创建(实例创建时机比较早)
static class Singleton{
//把 构造方法 变为私有,此时在该类外部,就无法 new 这个类的实例了
private Singleton(){
}
//再来创建一个 static 的成员,表示Singleton 类唯一的实例
//static 和 类相关,和实例无关,类在内存中只有一份,static 成员也就只有一份
static Singleton instance = new Singleton();
//new没报错是因为Singleton类是singlePattern的内部类,singlePattern是可以访问内部类的private成员的
public static Singleton getInstance(){
return instance;
}
public static void main(String[] args) {
//此处得 getInstance 就是获取实例得唯一方式,不应该使用其他方式创建实例了
Singleton s = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s == s2);
}
}
}
只要类被加载,就会立刻实例化Singleton实例,后续无论怎么操作,只要严格使用getInstance,就不会出现其他实例。
public class lazyPattern {
//使用懒汉模式来实现,Singleton类被加载的时候,不会立刻实例化
//等到第一次使用这个实例的时候,再实例化
static class Singleton{
private static Singleton instance = null;
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
public static void main(String[] args) {
}
}
类被加载的时候,没有立刻被实例化,第一次调用getInstance的时候,才真正的实例化。
如果要是代码一整场都没有调用getInstance,此时实例化的过程也就被省略掉了,又称“延时加载”
一般认为“懒汉模式” 比 “饿汉模式”效率更高。
懒汉模式有很大的可能是“实例用不到”,此时就节省了实例化的开销。
各种编辑器,有两种主要的打开方式:
安全。
类加载只有一次机会,不可能并发执行,对于饿汉模式来说,多线程同时调用getInstance,由于getInstance里只做了一件事:读取instance实例的地址=》多个线程在同时读取同一个变量,不会产生线程不安全。
懒汉模式是线程不安全的,只有在实例化之前调用,存在线程不安全问题
如果要是已经把实例创建好了~后面再去并发调用getInstance 就是线程安全的了
以上面的懒汉模式代码为例,多线程并发执行的时间线如下。
1.加锁
public class lazyPatternToSafe {
static class Singleton{
private static Singleton instance = null;
//方法一:
public static Singleton getInstance1(){
synchronized (lazyPattern.Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
return instance;
}
//方法二:
synchronized public static Singleton getInstance2(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
}
上面的“粒度”较小,下面的“粒度”较大。
加锁之后的时间线:
但是加锁之后,仍然存在效率问题。上面的改进代码,哪怕实例已经创建好了,但是每次调用getInstance还是涉及加锁解锁,而这里的加锁解锁已经不必要了。只要代码中涉及到锁,基本上就和高性能无缘了(因为涉及到锁之间的等待)
2.解决方案:只有在实例化之前调用的时候加锁,后面不加锁~
static class Singleton {
private static Singleton instance = null;
public static Singleton getInstance1() {
if(instance == null){
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
上述双线程执行的时间线如下:
此时即能保证线程安全,也能保证程序运行的效率。
3.但是还是存在问题,此处的多个操作,可能会被编译器优化,只有第一次读才从内存中读取,后续的读就直接从CPU中读取寄存器。就可能导致线程1修改之后,线程2没有读到最新的值,内存可见性导致的线程不安全问题(先加锁的线程在修改,后加锁的线程在读取)。
最终版本:加volatile关键字
static class Singleton {
//为了解决内存不可见问题,需要加上关键字volatile
private volatile static Singleton instance = null;
public static Singleton getInstance1() {
if(instance == null){
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
上面的代码:为了保证线程安全,涉及到三个要点:
1.加锁 【保证线程安全】
2.双重if 【保证效率】
3.volatile【避免内存可见性引来的问题】