目录
知识铺垫
什么是设计模式?
通俗点讲就类似于"棋谱",有一些固定的套路,可以针对一些特殊场景给出比较好的解决方案。
而在开发过程中,便可以起到模版的作用。
什么是单例模式?
单例模式能保证某个类在程序中只存在唯⼀⼀份实例,⽽不会创建出多个实例。
⽐如JDBC中的DataSource实例就只需要⼀个。--->http://t.csdnimg.cn/IQ3cj
主流的单例模式有以下两种写法--->
饿汉模式,顾名思义,就像一个总是急着吃东西的"饿汉"一样,不管是不是马上需要用到它,就是提前创建好单例实例。
懒汉模式则是延迟创建单例实例,直到真的需要用到它的时候才去创建,就像一个总是等到最后一刻才行动的"懒汉"一样。
举个例子,每次吃完饭后,对于洗碗这件事,有些人就立刻把碗给洗干净。(饿汉模式)
另外有些人则是放水里泡着,等到下一顿饭需要碗装菜的时候再洗。要装几个菜才洗几个碗。(懒汉模式)
饿汉模式
class Singleton {
//在类加载时就创建单例实例
private static final Singleton instance = new Singleton();
//提供一个公共的访问方法,返回单例实例
public static Singleton getInstance() {
return instance;
}
//私有构造方法,防止外部实例化
private Singleton() {
}
}
仔细观察,instance成员变量就是"类成员",一开始就创建好,虽然还没人用,并且讲构造函数设为私有,只能通过访问getInstance()方法去获取instance。
这也就体现了单例模式的特点---确保不会随意new一个其他对象出来。
而且通过以下代码验证:单例模式确保了在整个应用程序中只有一个实例存在。
public class demo21 {
public static void main(String[] args) {
//获取单例实例
Singleton dog1 = Singleton.getInstance();
Singleton dog2 = Singleton.getInstance();
//检查两个实例是否相同
if (dog1 == dog2) {
System.out.println("1");
} else {
System.out.println("2");
}
}
}
因此无论调用多少次 getInstance方法,返回的都是同一个实例,所以结果如图:
懒汉模式
class LazySingleton {
//声明一个静态的实例变量,但不立即创建实例
private static LazySingleton instance;
//私有构造方法,防止外部实例化
private LazySingleton() {
}
//提供一个公共的静态方法,返回单例实例
public static LazySingleton getInstance() {
//如果实例还未创建,则创建它
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
和饿汉相比,懒汉模式需要用到的时候才会创建实例,即推迟了实例的创建。
所以不会一开始就new。只有在需要访问instance 的时候,才会创建实例。
其他和饿汉大同小异。加以验证。
public class demo22 {
public static void main(String[] args) {
SingleLazy dog1 = SingleLazy.getInstance();
SingleLazy dog2 = SingleLazy.getInstance();
if (dog1 == dog2) {
System.out.println("1");
} else {
System.out.println("2");
}
}
}
懒汉模式的线程安全问题
这里只谈论懒汉模式,因为饿汉模式不存在线程安全问题---没有涉及到线程的切换。
忘记了线程安全问题的相关知识点可以回顾一下-->http://t.csdnimg.cn/QC4er
仔细观察懒汉模式下的代码。
class LazySingleton {
//声明一个静态的实例变量,但不立即创建实例
private static LazySingleton instance;
//私有构造方法,防止外部实例化
private LazySingleton() {
}
//提供一个公共的静态方法,返回单例实例
public static LazySingleton getInstance() {
//如果实例还未创建,则创建它
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
这里有个逻辑是先判断再创建实例,但这两者之间可能会涉及到线程的切换,从而引发问题。
比如有2个线程t1,t2.
首先有一点是线程是随机调度,抢占式执行的。
在t1线程执行判断语句的时候, 就可能跳到t2线程,也符合条件,创建了一个实例。
但是回到t1线程也会创建一个新的实例(符合判断条件),所以就会覆盖第一次的实例。而原本的实例就会被回收掉了。
加锁
原因可以总结成if和new之间出现了线程切换。因此可以把它们打包成一个整体。
public static SingleLazy getInstance() {
synchronized (lock1) {
//判断是否创建实例对象
if (instance == null) {
instance = new SingleLazy();
}
}
return instance;
}
重复加锁对性能的影响
但也有新的问题,线程是安全了,但 instance一旦创建好之后,if语句不再执行。
但每次调用getInstance()去访问的时候,都会触发加锁操作。这样便可能引起堵塞,影响性能。
即还要判断一下要加锁的时候加锁,可以不用加锁的时候就不加锁。如图:
public static SingleLazy getInstance() {
//判断是否加锁
if (instance == null) {
synchronized (lock1) {
//判断是否创建实例对象
if (instance == null) {
instance = new SingleLazy();
}
}
}
return instance;
}
看到这里是不是很懵?怎么会有两个一模一样的if(instance==null)?
但只是写法相同,实际作用则不同。
**第一个if语句判断是否要加锁。**因为刚刚分析过,已经创建好实例的话就没必要次次调用getInstance()去访问的时候,都触发加锁操作。
**第二个if 语句则是判断是否要创建实例对象。**这个就是最开始的if语句。
指令重排序问题
到这里看似没有问题了,但想想创建一个实例对象需要几步?
1.分配内存空间。
2.初始化对象。
3.将引用指向分配的内存空间。
举个栗子~
想象你正在准备开一家新店。这个过程可以分为三个主要步骤:
-
选址:这就像是在内存中为你的对象"分配空间"。你找到了一个合适的地方。
-
装修:这就像是"初始化对象"。你开始装修店铺。
-
开门营业:这就像是"将引用指向分配的内存空间"。你完成了所有的准备工作,现在人们可以通过地址找到你的店铺,进来购物。在编程中,这个"地址"就是对象的引用,告诉程序去哪里找到这个对象。
但在多线程环境中,顺序可能发生调换,可能是1 3 2 的执行顺序。这样一来,又可能导致线程切换的问题。
所以安全起见,还要加上volatile关键字。
class SingleLazy {
private static final Object lock1=new Object();
//使用volatile关键字
private static volatile SingleLazy instance;
//私有构造函数,防止外部实例化
private SingleLazy() {}
public static SingleLazy getInstance() {
//判断是否加锁
if (instance == null) {
synchronized (lock1) {
//判断是否创建实例对象
if (instance == null) {
instance = new SingleLazy();
}
}
}
return instance;
}
}
同样加以验证
public class demo22 {
public static void main(String[] args) {
SingleLazy dog1 = SingleLazy.getInstance();
SingleLazy dog2 = SingleLazy.getInstance();
if (dog1 == dog2) {
System.out.println("1");
} else {
System.out.println("2");
}
}
}
看到最后,如果觉得文章写得还不错,++希望可以给我点个小小的赞++,您的支持是我更新的最大动力