声明:原创作品,转载请注明出处https://www.jianshu.com/p/b99e870f4ce0
有的时候,我们需要某个类只能被实例化一次,那么我们就可以使用这种模式。单例模式是相对来讲比较简单的一种设计模式,虽说简单,却是处处暗藏杀机。首先我们来看下一个类如何才能只被实例化一次。我们知道一般实例化对象时,都会调用类的构造方法。如果构造方法为public,那么肯定每个人都能实例化这个类,也就无法保证该类对象的唯一性。所以这个类的构造方法必须为private,不能向外界提供。但是这样我们就无法调用它的构造方法,也就无法实例化对象了,那么由谁来实例化呢?想必你也想到了,由类自身调用,因为这时候也只有它自身能调用构造方法了。我们看下代码:
public class Singleton {
private Singleton(){}
public Singleton getInsatnce(){
return new Singleton();
}
}
我们在类中定义一个getInstance方法来提供一个该类的实例化对象,不过有个问题,我们该如何调用这个方法呢,因为只有在该类实例化后才能调用这个方法,然而这个方法就是用来实例化对象的。是不是很纠结,有什么好办法吗?很简单我们只要将这个方法表示成静态方法就可以:
public class Singleton {
private Singleton(){}
public static Singleton getInsatnce(){
return new Singleton();
}
}
这样我们就可以直接调用Singleton.getInstance()来创建对象了。
恶汉式单例模式
可是这也不是唯一的啊,每次调用这个方法都会新建一个实例对象。别担心,我们只要稍微改变下就好了:
public class Singleton {
private static Singleton singleton = new Singleton();
private Singleton(){}
public static Singleton getInsatnce(){
return singleton;
}
}
我们在类中事先就创建好一个实例对象,每次调用getInstance方法时返回这个对象就可以了。这样我们的单例模式就诞生了。我们可以看到,只要这个类一加载完,就会创建实例对象,显得很着急,像一个恶汉一样,所以我们称之为恶汉式单例模式。
懒汉式单例模式
上面的恶汉式单例模式会带来一个问题,假如我们代码中都没用到这个类的实例对象,如果这个类简单还好,要是复杂的话就会造成大量资源浪费。这时你可以用懒加载的方式来创建对象,即当你要使用这个实例对象时再创建。代码如下:
public class Singleton {
private static Singleton singleton ;
private Singleton(){}
public static Singleton getInsatnce(){
if (singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
由于只有当用到时才创建对象,比较懒,我们称之为懒汉式单例模式。
线程安全的懒汉式单例模式(双重检验锁模式)
以上懒汉式单例模式虽然可以延迟创建实例,不过会带来新的问题,我们先看个简单的例子:
我们在上述类的构造方法中加入一句打印语句,如果该类被实例化就会打印出日志。如下:
public class Singleton {
private static Singleton singleton;
private Singleton(){
System.out.print("实例化对象\n");
}
public static Singleton getInsatnce(){
if (singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
然后新建一个测试类来实例化该类:
public class TestClient {
public static void main(String[] args){
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
Singleton.getInsatnce();
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
Singleton.getInsatnce();
}
});
thread1.start();
thread2.start();
}
}
测试类也很简单,创建了两个线程,每个线程都调用Singleton的getInstance方法来获取对象。好了我们运行该测试类看下:
输出结果:
-------------------------------------------
实例化对象
-------------------------------------------
正如我们所愿,虽然我们调用两次getInstance,但该类只被实例化一次。但不要高兴的太早,你多运行几次后会发现,有的时候会打印两遍,也就说该类被实例化了两遍:
输出结果:
-------------------------------------------
实例化对象
实例化对象
-------------------------------------------
这是为什么呢?我们明明已经做了判断,如果对象为空就创建,不为空就直接返回,怎么还会创建两遍呢?其实这正是多线程在作怪。
我们知道,程序的运行其实就是CPU在一条条的执行指令,如果是单线程,那么CPU就会依次执行该线程中的指令,但如果是多线程,比如有两个线程,线程A和线程B。为了让两个线程同时执行,那么CPU会执行A线程一段时间然后暂停,去执行B线程的指令,一段时间后再切换到A线程执行。这样反复切换直到程序结束,由于CPU切换的速度很快,所以让人感觉两个线程是同时执行的。那么到底CPU什么时候切换,以及每次切换执行多久呢,其实这都是随机的,一般来讲每个线程被执行到的几率都差不多,不过你可以提高某个线程的优先级来让CPU多执行你会儿,但什么时候切换你是无法控制的,只能提高你被执行到的几率。
好了,回到上面的例子,我们来看下这个问题到底是怎么产生的。为了便于说明我在Singleton类中标记了两行代码,这两行代码也正是问题的关键:
public class Singleton {
private static Singleton singleton;
private Singleton(){
System.out.print("实例化对象\n");
}
public static Singleton getInsatnce(){
if (singleton == null){ //-----------------------------------1
singleton = new Singleton(); //--------------------------- 2
}
return singleton;
}
}
假设现在有两个线程A和B,首先A线程调用getInstance方法,当执行到语句1时会判断对象是否为空,由于该类还没被实例化,所以条件成立,遍进入到花括号中准备执行语句2,正如前面所说线程的切换是随机,当正准备执行语句2时,线程A突然停在这里了,CPU切换到线程B去执行。当线程B执行这个方法时,也会判断语句1的条件是否成立,由于A线程停在了语句1和2之间,实例还未创建,所以条件成立,也会进入到花括号中,注意此时线程B并未停止,而是顺利的执行语句2,创建了一个实例,并返回。然后线程B又切换回了线程A,别忘了,这时,线程A还停在语句1和2之间,切换回它的时候就又继续执行下面的代码,也就是执行语句2,创建了一个实例,并返回。这样,两个对象就被创建出来了,我们的单例模式也就失效了。
好了,找到原因了,那有什么方法解决吗?很简单只要在getInstance方法前加上关键字synchronized
就可以了。代码如下:
public class Singleton {
private static Singleton singleton;
private Singleton(){
System.out.print("实例化对象\n");
}
public static synchronized Singleton getInsatnce(){
if (singleton == null){ //-----------------------------------1
singleton = new Singleton(); //--------------------------- 2
}
return singleton;
}
}
用synchronized
修饰这个方法,相当于给这个方法加了把锁,只要有线程进入到这个方法里面,那么这个锁就会被锁上,这时其他的线程想要执行这个方法时,一看,呦,厕所门关着待会再来上。只有当里面的线程执行完这个方法后,这个锁才会打开,其他线程才能进入。这样就很好的避免前面重复创建对象的问题。synchronized虽然解决了这个问题,但是synchronized会降低程序执行效率,试想你开车到某地有两条路,突然其中一条在维修,被封锁了,那就势必会造成另一条路的拥堵。看来我们还得再优化下上面的代码。
由上面的分析我们知道,这个实例的重复创建问题主要是在实例还未被创建的时候,且是在执行语句1,2时产生的,只要实例创建成功了,就没有必要加锁了。换句话说,我们没有必要给整个getInstance方法加锁,其实只用在实例还未创建时给语句1和语句2加个锁就可以了,当实例创建成功后会直接返回实例。优化后的代码如下:
public class Singleton {
private static Singleton singleton;
private Singleton() {
System.out.print("实例化对象\n");
}
public static Singleton getInsatnce() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
这里synchronized 的用法和上面的有所不同,上面我们用synchronized 来修饰方法,表示给整个方法上了把锁,我们称之为同步方法。这里我们只给语句1和语句2加了把锁,我们称这种结构为同步代码块,同步代码块synchronized 后面的括号中需要一个对象,可以任意,这里我们用了Singleton的类对象Singleton.class
。可以看到我们在方法中进行了两次对象是否为空的判断,一次在同步代码块外面,一次在里面。因此称之为双重检验锁模式(Double Checked Locking Pattern)
。为什么要判断两次呢,当还未实例化的时候,就进行同步创建对象,为什么同步代码块里面还要做次判断呢?我们来分析下如果里面不做判断会怎么样:
public class Singleton {
private static Singleton singleton;
private Singleton() {
System.out.print("实例化对象\n");
}
public static Singleton getInsatnce() {
if (singleton == null) {//-----------------------------1
synchronized (Singleton.class) {//-----------2
singleton = new Singleton(); //----------3
}
}
return singleton;
}
}
如上,我们在同步代码块中去掉了判断语句,这时有两个线程A、B调用getInstance方法。假设A先调用,当A调用方法时,会执行语句1进行条件判断,由于对象尚未创建,所以条件成立,正准备执行语句2来获取同步锁。我们上面也分析过了,线程的切换是随机的,还未执行语句2时,线程A突然停这了,切换到线程B执行。当线程B调用getInstance方法时也会执行语句1进行条件的判断,由于这时实例还未创建,所以条件成立,注意这时线程B还是没有停,又继续执行了语句2和3,即获取了同步锁并创建了Singleton对象。这时线程B切换回A,由于A此时还停在语句1和2之间,切回A时,就又继续执行语句2和3,即获取同步锁并创建了Singleton对象,这样两个对象就被创建出来了,synchronized 也失去了意义。所以我们需要在同步代码块中再做次判断:
public class Singleton {
private static Singleton singleton;
private Singleton() {
System.out.print("实例化对象\n");
}
public static Singleton getInsatnce() {
if (singleton == null) {//-----------------------------1
synchronized (Singleton.class) {//------------2
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
这样当线程A从语句1和2之间醒来,然后获取到同步锁后在创建对象前做一个判断,如果对象为空就创建,如果不为空就直接跳出同步代码块并返回之前线程B创建的对象。这样当下次再调用getInstance方法时,由于之前创建过对象,就不会再进入到同步代码块中,而是直接返回实例。我们代码的执行效率也就上去了。好了现在我们的双重检验锁模式
既解决了在多线程中重复创建对象问题,又提高了代码执行效率,同时还是懒加载模式,是不是已经非常完美了?别高兴的太早,其实这还是有问题的。纳尼!!搞了这么久怎么还有问题,有的朋友可能已经坐不住准备退票了。你先别急,让我们看看到底哪里还有问题。其实问题就出在Singleton的创建语句上:
singleton = new Singleton();
为什么这句会有问题呢,在分析原因之前,我们先来了解下Java虚拟机在执行该对象创建语句时具体做了哪些事情,我们简单概括为3步:
- 1 在栈内存中创建singleton 变量,在堆内存中开辟一个空间用来存放Singleton实例对象,该空间会得到一个随机地址,假设为0x0001。
- 2 对Singleton实例对象初始化。
- 3 将singleton变量指向该对象,也就是将该对象的地址0x0001赋值给singleton变量,此时singleton就不为null了。
我们之前说过,程序的运行其实就是CPU在一条条执行指令,有的时候CPU为了提高程序运行效率会打乱部分指令的执行顺序,也就是所谓的指令重排序,当然这种指令重排序并不改变最后的运行结果。我们上面的3步就包含了大量的CPU指令,当CPU执行时,是无法保证一定是按照123的顺序执行,也可能由于指令重排序的优化,会以132的顺序执行。假设现在有两个线程A、B,CPU先切换到线程A,当执行上述创建对象语句时,假设是以132的顺序执行,当线程A执行完3时(执行完第3步后singleton就不为null了),突然停住了,CPU切换到了线程B去调用getInstance方法,由于singleton此时不为null,就直接返回了singleton,但此时步骤2是还没执行的,返回的对象还是未初始化的,这样程序也就出问题了。那有什么方法解决吗?很简单只要用volatile
修饰singleton变量就可以了:
private volatile static Singleton singleton;
那为什么volatile 修饰变量就可以了呢,我们上面提到指令的重排序,其实CPU在执行指令时并不是无节操随意打乱顺序,而是有一定的原则可寻的,这个原则也叫先行发生原则(happens-before)
,只要不符合这个原则,那么执行顺序也是得不到保障的,具体有以下8条原则:
先行发生原则(happens-before):
- 1 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
- 2 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
- 3 volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
- 4 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
- 5 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
- 6 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 7 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
- 8 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
这些原则你可能不太理解,不过没关系,这里我们重点关注第3条:即,对volatile变量的写操作先行与对这个变量的读操作。我们知道上面的问题主要是线程B对Singleton 对象读取时,该对象还未写入初始化导致的,那么如果我们用volatile来修饰的话,就不会出现这种情况,我们的虚拟机在读取该对象时,会保证其一定是写入好的,也就是不会出现132这种情况。这样也就不会出现问题啦。我们来看下完整的代码:
public class Singleton {
private volatile static Singleton singleton;
private Singleton() {
System.out.print("实例化对象\n");
}
public static Singleton getInsatnce() {
if (singleton == null) {//-----------------------------1
synchronized (Singleton.class) {//------------2
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
好了,这就是完美的双重检验锁的单例模式啦,放心,现在绝对没坑了。不过每次写单例模式都要写这么多,也是挺麻烦的,有简单点的吗?当然有了,下面介绍两种比较简洁的单例模式,即用静态内部类和枚举来实现,我们来简单了解下。
静态内部类实现单例模式
public class Singleton {
private static class SingletonHolder {
private static final Singleton singleton = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.singleton;
}
}
可以看到,我们在Singleton 类内部定义一个SingletonHolder 静态内部类,里面有一个对象实例,当然由于Java虚拟机自身的特性,只有调用该静态内部类时才会创建该对象,从而实现了单例的延迟加载,同样由于虚拟机的特性,该模式是线程安全的,并且后续读取该单例对象时不会进行同步加锁的操作,提高了性能。
枚举实现单例模式
接下来,我们看下如何用枚举来实现单例。可能有的朋友对枚举还不是很熟悉,其实枚举和我们的普通类没有太大区别,比如我们下面举个简单的例子:
public class Person {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
上面,我们定义了一个简单的Person类,类中定义了一个属性name,非常简单,接下来,如果你想要操作这个类,比如创建一个person对象,并写入对象的name然后再获取name,也是非常简单:
Person person1 = new Person();
person1.setName("张三");
System.out.print(person1.getName());
这里,想必你只要接触过Java语言都能看懂上面的代码,我们创建了一个Person对象,并给对象设置了一个名字,然后打印出名字。
接下来我们看下用枚举如何完成上面的操作,我们把上面的Person类稍加修改:
public enum Person {
person1;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
你可能发现了不同的地方,我们把Person类名前的class改为了enum,表示这个Person类是个枚举类,然后你会发现,这个类中比之前的类多了一行代码
person1;
这个person1是什么,你发现前面我们实例化Person的时候也出现过person1,是的你没猜错,这里的person1就是我们这个枚举类的一个对象实例,也就是说,如果你要获取一个Person对象,不用再像上面那样调用new Person()来创建对象,直接获取这个枚举类中的person1就可以了,这个person1就是一个Person对象实例,你可能不信,没关系我们来试验下:
Person.person1.setName("张三");
System.out.print(Person.person1.getName());
运行后你会发现,成功打印出来名字,是不是很方便。可能你会说,那我想再创建一个Person对象比如叫做person2,怎么办呢,很简单,只要在person1后面再加上person2
就可以了,如下:
person1,person2;
注意:实例person1和实例person2之间要用逗号隔开,用分号结束,且实例要定义在类中的第一行。
好了,了解的枚举的简单实用,问题来了如何将上述的枚举Person类改为单例呢,很简单,我们只要在类中中定义一个实例就可以了,比如去掉person2,只保留person1,如下:
public enum Person {
person1;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
这样你就只能获取到一个Person实例对象了。可能有的人有疑惑了,不对啊,难道我就不能再new一个吗?这个是不能的,因为枚举类的构造方法是私有掉的,你是无法调用到的并且你也无法通过反射来创建该实例,这也是枚举的独特之处。可能有人会问了,如果这个Person的name需要在对象创建时就初始化好,那该怎么办呢?很简单,就和普通类一样只要在里面定义一个构造方法,传入name参数就可以了。如下:
public enum Person {
person1("张三");
private String name;
Person(String name){
this.name = name;
}
public String getName() {
return name;
}
}
可以看到就和普通类一样,我们在枚举类中定义了一个入参为name的构造方法,注意这构造方法前面虽然没有加权限修饰的,但并不表示它的权限是默认的,前面提到枚举类中的构造方法是私有的,即使你强行给它加个public,编辑器也会报错。好了,定义好了构造方法,就可以调用。调用也会简单,直接在实例person1后面加个括号传入一个名字就可以了,这样Person中的name字段就有值了,你可以直接调用Person.person1.getName()来获取这个名字,是不是很方便。
另外枚举类实例的创建也是线程安全的,所以使用枚举来实现单例也是一种比较推荐的方法,但是如果你是做Android开发的,你需要注意了,因为使用枚举所使用的内存是静态变量的两倍之上,如果你的应用中使用了大量的枚举,这将会影响到你应用的性能,不过一般用的不多的话还是没什么关系的。
好了,单例模式到这里也介绍的差不多,你可以根据自己的喜好以及具体的业务选择其中一种。
设计模式持续更新中...