单例设计模式————只能产生一个实例对象。
与其说单例设计模式是一种设计模式,个人更多的认为它是Java语言规则的一种描述。
为什么这么说,因为单例设计模式的实现机制离不开Java语言的三个特点:访问权限控制,static关键字,类构造方法。
我们先说访问权限控制,Java中访问权限控制有public,protected,private,外加上默认访问权限default。
这几个关键字的“权利范围”不在本博客的讨论范围之内。
我们只需知道,如果一个类对于它自身的属性进行了权限处理,我们访问他的属性除了访问他提供的相关接口,我们将束手无策。
我们做个简单的代码示例:
public class PowerExample {
public static void main(String[] args) {
Person lisi = new Person();
lisi.country = "china";
lisi.height = 160;
lisi.setName("lisi");
lisi.setScore(80);
//public属性
System.out.println(lisi.country);
System.out.println(lisi.height);
//private
System.out.println(lisi.getName());
System.out.println(lisi.getScore());
}
}
class Person {
private String name; //姓名
private int score; //分数
private String fancy; //喜好
public String country; //国籍
public int height; //身高
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getScore() {
return score + 10;
}
public void setScore(int score) {
this.score = score;
}
}
运行时候输出:
china
160
lisi
90
从代码中可以看出,
对于声明为public的属性,我们可以直接通过"对象."的方式来填充或者访问属性的信息,如country,height;
对于声明为private的属性,如果对象有提供对外的接口,我们可以通过调用对应的接口来填充或者访问属性的信息,如name,score,
当然这个信息并不一定就是真实的信息,比如score;
如果一个属性是private,同时也没有提供外部接口,那我们将无法获取它的信息,如fancy;
总的来说,访问权限的存在使得外部类只能通过类允许的方式来操纵和访问这个类,同时他也实现了类的完美封装。
接下来我们来说明构造方法。
构造方法干了一件什么事儿呢?
他规定了我们创建这个类的方式;从另一角度来说,构造方法也是对对象的一个初始化。
如果我们没有显示声明类的构造方法,那么创建类的时候类会调用一个默认的构造方法;
如过我们声明了构造方法,这意味着我们明确的知道我们想要构造一个什么样的类————所以,我们构建的类也将是我们期望的那样。
如果我们声明了构造方法,并且构造方法里面有一系列的参数,但是我们创建类对象的时候没有传入需要的参数,
那么类就不干了————你规定了创建对象的方式,你自己却不按照套路出牌,你想闹哪样?
是的,报错。
我们看报错提示:
提示:要么添加参数以匹配现有的构造函数;要么修改构造函数为不含参数;要么重载构造函数(不带参数)。
static的作用比较特殊。
对于类而言,属性和方法都是类的一部分,一般情况下是不能脱离对象单独存在。
也就是说,我们想访问方法或者属性,是通过创建对象实例,然后通过实例对象调用。
但是static关键字修饰的属性或者方法在这个规则之外。
我们都知道Java程序中main方法是程序入口,那我们有创建main方法所在类的对象实例么?
首先,对于static关键字修饰的属性或者方法在类加载之后,我们可以直接通过"类名.static修饰的属性或者变量"的方式来访问。
其次,static修饰的属性和方法在内存中只存在一份。
其实可以这么理解,static关键字完整了Java语言。
为什么这么说?我们假设一下,如果没有static关键字,程序怎么去执行?
我们都知道main方法作为程序入口,那么想执行这个类,是不是要创建main方法所在类对象,然后通过这个对象.main()的方式访问?
那么我们要在什么位置执行这条语句呢?是不是也在方法里面?那我们怎么调用这个方法呢?继续创建这个方法所在的类的对象。。。
然后,在创建。。。
有了staic关键字,不用创建对象就可以调用main方法,这个循环也就终止了。
这里简单说下类实例化时候的执行过程,想了解更多的话就需要学习JVM了。
在类实例化的过程中,JVM进行加载,分配内存,类初始化。
public class PowerExample {
public static void main(String[] args) {
System.out.println("测试加载顺序开始++++++");
StaticTest staticTest = new StaticTest();//显示加载顺序
System.out.println("测试加载顺序结束++++++");
System.out.println("测试static开始++++++");
System.out.println(StaticTest.j); //访问static变量
StaticTest.show(); //访问static方法
System.out.println("测试static结束++++++");
}
}
class StaticTest {
int i = 10;
static int j = 0;
static {
System.out.println("static 语句块");
}
{
System.out.println("实例代码 语句块");
}
public StaticTest() {
System.out.println("构造方法");
}
public static void show() {
System.out.println("static show()");
}
}
即使不运行程序,我们也可以分析出输出语句的执行顺序:
static语句块>实例化语句块>构造方法语句块。
在类加载完成之后staic语句块就会执行,分配内存时候也是对基础对象属性初始化,我们对属性赋值和实例代码块也会被加载到构造函数中,
它的位置在超类构造函数之后,自身初始化语句之前,所以实例化代码块的输出是在构造方法之前的。
运行输出:
测试加载顺序开始++++++
static 语句块
实例代码 语句块
构造方法
测试加载顺序结束++++++
测试static开始++++++
0
static show()
测试static结束++++++
下面我们考虑上面三个Java语言的特点如果两两结合或者三者结合会发生什么?
<1>访问权限+构造方法
<2>访问权限+static
<3>static+构造方法
<4>访问权限+构造方法+static
<1>public我们不再测试,主要测试private,看代码:
可以看到如果将构造方法声明为private,外部将不能直接创建类对象,编译器提示我们更改构造方法的可见性。
<2>访问权限和static
这个略。。
<3>static和构造方法
显然,用static修饰构造方法会报错,提示显示only public, protected & private are permitted。
我们可以分析一下为什么static不能修饰构造方法。
static修饰的属性或者方法是在类加载时候就可以发生的动作,而构造方法是对类进行的初始化,类的初始化是在内存分配以后,如果用static修饰了构造方法,意味着在JVM在类加载的同时要完成类的初始化,显然这是不被允许的。
<4>访问权限+构造方法+static
在<1>中,我们将构造方法声明为private,这意味着外部不能直接通过new的形式来创建出这个类的对象,
如果我们外部还想使用这个类的对象,怎么办?
不管构造方法声明为那种权限,影响的只是类外,类自身里面的方法是不会受到影响的,类本身的其他方法仍然可以调用构造方法,这就意味着其他方法是可以创建这个类的对象的。
那么我们用其他方法调用构造方法,同时对外提供一个接口,并将此接口声明为static,这时我们看到了什么?
没错,我们看到了单例设计模式:
public class PowerExample {
public static void main(String[] args) {
Single single = Single.getSingle();
single.show();
}
}
class Single {
private Single(){};
private static Single single = new Single();
public static Single getSingle() {
return single;
}
public void show() {
System.out.println("single.show().");
}
}
我们来做个测试,看是否内存中是否确实只有一份:
public class PowerExample {
public static void main(String[] args) {
Thread[] testSingles = new Thread[5];
for (int i = 0; i < testSingles.length; i++) {
testSingles[i] = new SingleThred();
}
for (int i = 0; i < testSingles.length; i++) {
testSingles[i].start();//调用run()方法
}
}
}
class SingleThred extends Thread {
public void run() {
System.out.println(Single.getSingle().toString());
}
}
class Single {
private Single(){};
private static Single single = new Single();
public static Single getSingle() {
return single;
}
}
我们创建一个线程类,然后覆盖run()方法,查看控制台输出:
当然以上实现也有另外一种实现——通过static语句块:
class Single {
private Single(){};
private static Single single = null;
static {
single = new Single();
}
public static Single getSingle() {
return single;
}
}
两种写法本质一样的,优劣也很明显:
容易理解,实现简单,类加载时候完成实例化。但同时也会造成一些内存浪费——不管我用不用这个实例,都会完成实例化。
我们试图对他进行改造:
class Single {
private Single(){};
private static Single single = null;
public static Single getSingle() {
if (null == single) {
single = new Single();
}
return single;
}
}
对于这种写法,看起来可能会更合理——我调用时候加载实例。
但可能会存在其他隐患,我们做个测试:
我们从结果中看到:内存中实例并不是同一份。
我们分析一下原因:
当一个线程想要获取到一个single实例之后,会进判断if (null == single)
如果single为null,就会开始试图实例化一个对象。
如果在这个当口——这个线程执行完判断之后试图实例化对象的之前,另一线程也进入了判断,这个时候上个线程尚未实例化,所以下个线程判断之后也会试图去实例化一个对象。
这样导致的结果是——单例设计失效。
我们对他继续进行改造——加锁:
从结果来看,这种加锁的方式可行。
分析优劣:
好处当然是解决了懒加载时候实例不唯一的情况,不好的地方就是对方法加锁会导致运行效率变低。
我们继续改进——尝试对代码块加锁:
这种虽然是对代码块加锁,但是作用于class对象,相当于锁住了全部的代码,效率同样降低。
继续改造——实例化的时候加锁:
同样存在隐患:当一个线程进入判断,然后开始执行同步时候,另一个线程进入判断。
继续改造——加锁之后在加一层判断:
测试通过。
我们继续改造。
通过以上分析,我们知道对单例设计可能的缺陷就是并发执行时候实例不唯一,我们采用的解决方式是加锁。
除了加锁之外呢?
未完待续。。