单例设计模式-------懒汉式,饿汉式(超详细,附代码)
单例设计模式-------懒汉式,饿汉式
单例设计模式是一种很常见的设计模式
在这里介绍两种单例设计模式 懒汉式与饿汉式
单例设计模式的特点:
1.单例设计模式保证一个类只有一个实例。
2.要提供一个访问该类对象实例的全局访问点。
单例设计模式要点
对一些类来说,只有一个实例是很重要的。例如很多时候对于某个系统只需要拥有一个全局对象,这样有利于我们协调系统的整体行为。
再比如说某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象
再通过这个单例对象获取这些配置信息。从而简化了在比较复杂的环境下配置管理。
通过上面的介绍,我们可以知道单例模式最重要的就是要保证一个类只有一个实例并且这个类易于被访问,那么要怎么做才能保证一个类具有一个实例呢?一个全局变量使得一个对象可以被访问,但是这样做却不能防止你实例化多个对象。
一个更好的办法就是,让该类自身负责保存它的唯一实例。并且这个类保证没有其他的实例可以被创建。
怎样保证一个对象的唯一总结如下:
1.为了避免其它程序过多的建立该类的对象,先禁止其它程序建立该类对象实例(将构造器私有化)
2.为了方便其它程序访问该类的对象,只好在本类中自定义一个对象,由1可知该对象是static的,并对外提供访问方式。
分析举例
在JAVA中单例设计模式
1.饿汉式如下所示
/** * Created by ${wuyupku} on 2019/3/15 12:39 */
class Singleton01{
private static Singleton01 modle = new Singleton01();//声明对象同时私有化
private Singleton01(){}//构造函数私有化
public static Singleton01 getInstance(){//向外声明访问该类对象的方法
return modle;
}
}
2.懒汉式如下所示
/** * Created by ${wuyupku} on 2019/3/15 12:43 */
class Singleton02 {
private static Singleton02 modle = null;//声明对象,不实例化
private Singleton02() {}//构造函数私有化
public static Singleton02 getInstance(){//向外提供访问该类对象的方法
if (modle == null)
modle = new Singleton02();
return modle;
}
}
3.测试是否保证了对象的唯一性:
import com.sun.scenario.effect.impl.sw.sse.SSEBlend_SRC_OUTPeer;
/** * Created by ${wuyupku} on 2019/3/15 12:47 */
public class SingletonTest {
public static void main(String[] args) {
TestSingleton01();
TestSingleton02();
}
public static void TestSingleton01(){
System.out.println("饿汉测试");
Singleton01 one = Singleton01.getInstance();
Singleton01 two = Singleton01.getInstance();
System.out.println(one);
System.out.println(two);
System.out.println("饿汉one = two"+"\t"+(one == two));
}
public static void TestSingleton02(){
System.out.println("懒汉测试");
Singleton02 one = Singleton02.getInstance();
Singleton02 two = Singleton02.getInstance();
System.out.println(one);
System.out.println(two);
System.out.println("懒汉one = two"+"\t"+(one == two));
}
}
到此我们总结两点:
1.饿汉式这种方式加载类对象,我们称作:预先加载方式。
2.懒汉式这种方式加载类对象,我们称作:延迟加载方式。
**问题:**上述程序只是在只有主线程一个线程运行下的测试结果,要是在多线程环境下又会出现什么样的结果呢?这里预先说明一下
其实在多线程的环境下,饿汉式是没有问题的,但是懒汉式这种延迟加载的方式会出现线程安全问题的,下面我们通过例子加以说明。
4.在多线程环境下运行饿汉式与懒汉式
观察上述的结果我们看到在饿汉式的测试中两个线程所得到的对象时同一个对象,因为@后面的就是对象的HashCode它们相同保证了对象的唯一性,但是在懒汉式中所得到对象不是同一个对象,因为两个线程所对应的HashCode不同,导致它们不是同一对象。这是为什么呢?为什么饿汉式就保证了对象的唯一性了而懒汉式却不能保证对象的唯一呢?
这里我们要分析的就饿汉式和和懒汉式那两个对象了Singleton1,与Singleton2了。在ThreadSingleton1与ThreadSingleton2的run方法中是线程运行的代码块,他们分别调用了饿汉式(Singleton1) 与 懒汉式(Singleton2)中的getInstance方法。在饿汉式中在该类的方法getInstance被调用之前,该类的对象已经存在,假设有两个线程(t1,t2)在调用该方法,t1与t2无论怎样调用都不会产生新的对象,因为该对象在方法调用之前就存在了,是伴随类的生成而生成的。
相反在懒汉式的方法getInstance被调用之前,该类的对象不一定存在(至少在第一次被调用的时候该类的对象不存在),所以他每次调用之前要检测,若对象不存在则生成对象,对象存在则直接返回。
在多线程环境下问题就出现生成对象的时候,假设有两个线程(t1,t2)在调用该方法,考虑这种情况,就是当线程t1检测到该对象不存在的时候,正要准备创建线程(还没创建),而在这个时候CPU切换到t2上执行权交给t2,t2检测到该对象不存在,就生成了对象,t2线程结束了,执行权回到t1上,t1这个时候就不需要判断了,因为开始他判断了的,他就直接向下执行,生成了对象。这样就导致了该类生成了两个不同的对象,当然这种情况不一定会发生,但这种情况是绝对存在的,也许你运行10000次都不会出现,或许你一运行就会出现该问题。
那么我们要如何解决这个问题呢?从上面的分析我们可以很清楚的看到,就是要控制当一个线程在操作getInstance方法的时候,不要让另一个线程操作该方法,而该该类对象Singleton2的对象model被线程t1与线程t2所共享,我们将共享的资源称作临界资源,把每个线程访问临界资源的那段代码称作临界代码。在操作系统中我们知道可以通过为临界代码设置信号量,就可以保证安全的访问共享资源。
在JAVA语言中为了实现这种机制,提供了以下两个方面的支持:
>>1 为每个对象设置一个“互斥锁”,这表明,在每一个时刻只有一个线程持有该互斥锁,而其他线程若要获得该互斥锁,必须等到该线程(持有互斥锁的线程)将其释放。
>>2 为了使用这个“互斥锁”,在JAVA语言中提供了synchronized关键字,这个关键字即可修饰函数,也可以修饰代码,实际上可以将其理解为就是一个锁,当一个线程执行该临界代码的时候,用synchronized给该线程先上锁,其它线程进不来,当线程代码执行完了的时候有释放该锁,只不过释放锁是隐式的不需要显示的指明,随代码的执行完毕,锁自动的被释放。
在JDK5.0后提供了接口Lock,该接口实现了比synchronized方法和语句可获得更广泛的锁操作,而用该接口就可以显示的指定加锁 lock.lock() 和解锁lock.unlock() (lock为实现了Lock的对象实例)。 所以我们要将getInstance方法上锁,在代码或函数上加关键字synchronized修改懒汉式Singleton2中的getInstance代码,如下
测试是否懒汉式保证了对象唯一?
通过观察我们可以看到保证了对象的唯一,测试成功。
分析代码,其实可以做一些优化操作,将代码上锁操作了后,每次执行都要检测锁,效率不高,其实我们可以这样思考只有在对象被创建的时候我们才上锁,保证对象的唯一,一旦对象都存在了,以后操作还需要锁嘛,不需要了吧,好吧我们就再次优化代码如下:
总计
饿汉式:
对象预先加载,线程是安全的,在类创建好的同时对象生成,调用获得对象实例的方法反应速度快,代码简练。
懒汉式:
对象延迟加载,效率高,只有在使用的时候才实例化对象,但若设计不当线程会不安全,代码相对于饿汉式复杂,第一次加载类对象的时候反应不快。