单例设计模式

前言

在介绍单例设计模式之前,先简单介绍一下java的内存分析,可以帮助我们更好的了解相关内容。在此之前,先简单介绍几个相关概念

Stack(栈)

存放基本数据类型的数据和对象的引用及存放变量,如果存放的是基本数据类型(非static),直接将变量名和值存入stack中,如果是引用,只将变量名存入栈,然后指向它new的对象(存放在堆中)

Heap(堆)

存放new产生的数据

静态域

存放在对象中用static定义的静态成员(基本类型)

常量池

存放所有数据

案例

下面将模拟几个实例先说明在证明。(此外注意String不是基本数据类型)
Alt text
上面给出了一个实例,意在了解内存分配,接下来将以String为例说明

1
2
3
4
5
6
7
8
String str="abc";
String str1="abc";
String str2=str;
String str3=new String("abc");
String str4=new String("abc");
String str5=str3;
static String str6="abc";
static String str7=new String("abc");

对应的测试内存地址是否相等

1
2
3
System.out.println(str==str1);
System.out.println(str==str2);
System.out.println(str1==str2);

测试结果 true true true
说明String 是引用类型,而且上面方法产生的变量都只在栈中并且都是同一个地址

1
2
System.out.println(str==str3);
System.out.println(str3==str4);

测试结果:false false
说明:new 之后的对象地址存放在堆中,同时堆中每个new之后的对象地址都是不一样的

1
2
3
4
System.out.println(str==str6);
System.out.println(str3==str6);
System.out.println(str6==str7);
System.out.println(str3==str7);

测试结果:true false false false
说明:String类型的静态变量存放在栈中,而new之后的静态变量存放在堆中

关于内存分配,比较复杂,这里只简单验证其中一些,其他可以自己设计案例模拟

单例

顾名思义就是指唯一的对象实例
具体点说就是保证在整个应用程序的生命周期中,任何一个时刻,单例类的实例只存在一个,
通常其结构如下所示。
Alt text
入门案例:首先是单例类

1
2
3
4
5
6
7
8
9
10
11
package com.zwl.pojo;
public class Singleton {
private static Singleton singleton=new Singleton();
private Singleton(){}
public static Singleton getInstance()
{
return singleton;
}}

测试用junit4进行测试

1
2
3
4
5
6
7
8
@Test
public void test1(){
System.out.println("start test..");
Singleton obj1=Singleton.getInstance();
Singleton obj2=Singleton.getInstance();
System.out.println(obj1==obj2);
System.out.println("end test..");
}

测试结果:start test.. true end test..
说明,obj1与obj2是同一个实例,间接说明了单例的特性

至此我们对单例有了一个初步的形式上的认识了,那么它的特点是什么了?主要为三点
一:单例类确保自己只有一个实例(构造函数私有:不被外部实例化,也不被继承)
二:单例类必须自己创建自己的实例
三:单例类必须为其他对象提供唯一的实例

说到单例就必然涉及到多线程,不多说,先模拟一个多线程来说明单例中存在的并发问题。
友情提醒一下,上面用的是饿汉式创建单例,就一开始就创建了一个实例,显然浪费内存,接下来将用懒汉式创建单例
主体类代码:

1
2
3
4
5
6
7
8
9
10
11
private static Singleton singleton;
private Singleton(){ }
public static Singleton getInstance()
{
if(singleton==null){
singleton=new Singleton();
}
return singleton;
}

下面将用三个线程模拟,两个线程调用单例类,第三个线程启动前两个线程,测试用junit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class thread1 implements Runnable{
Singleton singleton;
public Singleton getSingleton() {
return singleton;
}
public void setSingleton(Singleton singleton) {
this.singleton = singleton;
}
@Override
public void run() {
singleton=Singleton.getInstance(); }}
public class Thread2 implements Runnable{
Singleton singleton;
public Singleton getSingleton() {
return singleton; }
public void setSingleton(Singleton singleton) {
this.singleton = singleton; }
@Override
public void run() {
singleton=Singleton.getInstance();
}}

简单说明一下,这两个线程目的是调用单例类(注意如果是单例,决不允许出现不同实例)
先贴出测试类:

1
2
3
thread3 t=new thread3();
Thread tt=new Thread(t);
tt.start();

这里我们通过控制第三个线程来构造并发,(这里采用数学中的极限思想)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class thread3 implements Runnable{
thread1 t1=new thread1();
Thread2 t2=new Thread2();
Thread t=new Thread(t1);
Thread t3=new Thread(t2);
@Override
public void run() {
long start=System.currentTimeMillis();
t.start();
t3.start();
long end=System.currentTimeMillis();
System.out.println(end-start); //并发的时间间隔
System.out.println(t1.getSingleton());
System.out.println(t2.getSingleton());
}

测试结果:0 null null
多次运行,又出现另一种结果:6
com.zwl.pojo.Singleton@8fdcd1
null和其他不同步结果
说明在很短的时间间隔类,一个居然已经有了实例,而另一个居然是空,显然不是单例,想想倘若在此时去操作该单例,可能就会是静态类发生变化,也就是并发问题了。

很自然。那么怎么去解决了,一种很通用的方法就是加锁实现同步

双锁机制

1
2
3
4
5
6
7
8
9
10
11
12
13
public static Singleton getInstance()
{
if(singleton==null)
{
synchronized(Singleton.class)
{
if(singleton==null){
singleton=new Singleton();
}
}
}
return singleton;
}

Synchronized关键字,声明使该线程在某处同步,也就是一个线程先占用,另一个线程被阻塞,显然如果不为空,就直接返回对象,对应第二个线程也就不执行if语句了,达到单例效果,可以想象如果为计数变量,同样可以通过二次加锁实现同步
但是实际还是出现不同步,原因是jvm的一个bug,它允许无序写入线程,导致不同步,当然我们可以通过申明volatile关键字。
为什么就可以解决双重解锁的bug了?
原因是volatile保证原子性,顺序性,即在双重加锁前保证多线程实现顺序性,举个例子吧,现在有两个线程t1和t2,分别执行b,c操作,如果乱序写入,可能出现t1正在操作b还没完成,t2操作c,致使单例失败,所以需要保证顺序性。
经过不断的测试,发现完全同步,只不过时间间隔为0,和null,null,至少是同步了~

接下来简单比较一下饿汉式和懒汉式的区别
从速度和反应时间上,饿汉式加载好,从资源利用率上懒汉式好,
饿汉式很明显不会出现多线程并发问题,所以是线程安全的,而懒汉式则是非线程安全的

关于单例模式的应用,笔者在当初学习过程中,遇到很多,学习总是一个不断压缩与重新认识的过程,像sql工具类,hibernate的懒加载,mybatis及 spring中的一些配置等等,往往把握不好,就会出现错误,这里只列举一些工具类,因为它是笔者接触的第一个单例应用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Private static Connection connection;
Private sqlhelper(){};
public static Connection mysqlconn() throws Exception {
try {
// 加载驱动
Class.forName("com.mysql.jdbc.Driver");
// 连接数据库
connection = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/software", "root", "");
System.out.println("获得数据库");
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return connection;
}

热评文章