一、概念
《Design Patterns: Elements of Reusable Object-Oriented Software》(即后述《设计模式》一书),由 Erich Gamma、Richard Helm、Ralph Johnson
和 John Vlissides 合著(Addison-Wesley,1995)。这几位作者常被称为"四人组(Group of Four)"。
创建型模式(5个):单例模式、工厂模式、抽象工厂模式、建造者模式、原型模式;
结构型模式(7个):适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式;
行为模式(11个):模板方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、职责链模式、访问者模式。
1、单例模式
核心:保证一个类有且只有一个对象(实例),并且提供一个访问该实例的全局访问点。
应用场景:Windows 的任务管理器(Task Manager);
Windows的回收站(Recycle Bin),在整个系统运行中,回收站一直维护着仅有的一个实例;
项目中,读取配置文件类,一般也只有一个类,没必要每次使用配置文件的数据,每次new一个对象去读取;
网站的计数器,一般也是采用单例模式,否则难以同步;
应用程序的日志,也是采用单例模式,这是由于共享文件的日志文件一直处于打开状态,因为只有一个实例去操作,否则内容不好追加;
数据库的连接池也是采用单例模式,因为数据库的连接的是一种数据库资源;
操作系统的文件系统,也是采用单例模式,因为一个操作系统只有一个文件系统;
Application 也是典型的单例模式;
在Spring中,每个Bean默认就是单例,优点是Spring容器都可以管理;
在servlet(Server Applet:小服务程序或服务连接器,Java编写的服务器端程序),每个servlet也是单例
在Spring MVC框架/struts1框架中,控制对象也是单例。
优点: 由于单例只生成一个示例,减少了系统的性能开销,当一个对象产生需要比较多的资源时,如读取配置,产生其他依赖对象时,则可以通过在应用启动时直接
产生一个单例对象,然后永久驻留内存的方式来解决。单例模式可以在系统设置全局的访问点,优化环共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理
5种实现方式:
主要有2种:
(1) 饿汉式(线程安全、调用效率,但是不能延时加载)
(2) 懒汉式(线程安全、调用效率不高,但是可以延时加载)
次要有3种:
(3)双重检测模式(由于JVM底层内部模型的原因,偶尔会出现问题,不建议使用)
(4)静态内部类式(线程安全、调用效率高,可以延时加载)
(5)枚举单例(线程安全、调用效率高,不能延时加载,但是可以防止反射和反序列化漏洞)
如何选用?
单例对象占用资源少,不需要延时加载: 枚举式 优于 饿汉式
单例对象占用资源大,需要延时加载:静态内部类式 优于 懒汉式
【初步认识】
/***
* 23-1:单例模式
* 主要有2种:
(1) 饿汉式(线程安全、调用效率,但是不能延时加载) (2) 懒汉式(线程安全、调用效率不高,但是可以延时加载) 次要有3种: (3)双重检测模式(由于JVM底层内部模型的原因,偶尔会出现问题,不建议使用) (4)静态内部类式(线程安全、调用效率高,可以延时加载) (5)枚举单例(线程安全、调用效率高,不能延时加载)
*/
package cn.sxt.pattern; /* 1)饿汉式(单例对象立即加载)
* static变量会在类加载时初始化,此时也不会涉及多个线程对象访问该对象的问题。虚拟机保证只会装载一次该类,肯定不会发生
* 并发访问的问题。因此synchronized关键字可以省略。
* 问题:如果只是加载本类,而不是调用getInstance(),甚至永远没有调用,则会造成资源浪费。
* */
class Singleton{
//第2步:私有化一个静态对象。类初始化时立即加载这个对象instance
private static Singleton instance=new Singleton();//instance没什么特殊含义,用s也行,实例化对象的名字 private Singleton() { //第1步:私有构造器,只有自己可以用 }
//由于加载类时,天然的线程安全,方法不用同步,调用效率高
public static Singleton getInstance() {
return instance;
}
} /* 2)懒汉式(单例对象延时加载,不立即加载,中间用的时候才去new一个新的对象)
* 延时加载,真正用的时候才去加载!
* 问题:资源利用率高,但是,每次调用getInstance()方法时,要使用并发,效率低
* */
class Singleton02{
private static Singleton02 s;
private Singleton02() {//私有化构造器 }
//为啥加同步?因为当线程A执行到s==null后,睡觉去啦,当线程B进来后发现s也是null,会去创建一个对象,当线程A醒来之后也去创建
//一个新的对象,这样就2个对象,违反单例的定义(即一个单例类有且只有一个示例(对象))
public static synchronized Singleton02 getInstance() {
if (s==null) {
s=new Singleton02();
}
return s;
}
} /*3)双重检测锁的实现(实际工作很少用),综合了懒汉和饿汉的模式
* 这个模式将同步的内容下放到if内部,提高了执行效率,不必每次获取对象时都进行同步,只有第一次才同步,创建对象后就没必要同步了
* 问题:由于编译器优化的原因和JVM底层内部模型原因,偶尔会出问题
* */ class Singleton03{
private static Singleton03 s=null;
private Singleton03() {
} public static Singleton03 getInstance() { if (s==null) {
Singleton03 sc;
synchronized (Singleton03.class) {
sc=s;
if (sc==null) {
synchronized (Singleton03.class) {
if (sc==null) {
sc=new Singleton03();
}
}
s=sc;
}
}
}
return s;
}
} /*4) 静态内部类的实现(也是一种懒加载)
* 外部类没有static属性,则不会像饿汉式那样立即加载对象
* 只有真正调用getInstance方法时才会加载静态内部类,加载类时线程是安全的,对象s是static final类型的,保证了内存中
* 只有一个实例存在,而且只能赋值一次,从而保证了线程的安全性
* 好处:兼备了并发高效调用和延迟加载的优势
*
* */ class Singleton04{
private static class SingletonClassInstance {
private static final Singleton04 s=new Singleton04();
}
//初始化Singleton04类时不会立即初始化静态内部类SingletonClassInstance,只能用到时才会通过SingletonClassInstance.s
//去调用对象s(这里也可以看作是它的属性),延时加载
public static Singleton04 getInstance() {
return SingletonClassInstance.s;//
}
private Singleton04() { }
} /*5)枚举方式(没有懒加载)
* 优点:实现简单,枚举本身就是单例模式。由JVM从根本上提供保障,避免通过反射和反序列的漏洞
* 缺点:无法延时加载
* */
enum Singleton05{//注意定义与class的区别 //定义一个枚举元素INSTANCE,它就代表Singleton05的一个单例对象
INSTANCE; //添加自己需要的操作
public void singletonOperation() { } } public class Test_0422_Singleton {
public static void main(String[] args) {
/*getInstance是一个函数,在java中,可以用这种方式使用单例模式创建类的实例,所谓单例模式就是一个类有且只有一个实例,
不像object ob=new object();的这种方式去实例化后去使用 */
/*Singleton s1=Singleton.getInstance();
Singleton s2=Singleton.getInstance();
System.out.println(s1==s2); //输出为true*/ Singleton02 s1=Singleton02.getInstance();
Singleton02 s2=Singleton02.getInstance();
System.out.println(s1==s2); //输出为true Singleton05 s5=Singleton05.INSTANCE;
Singleton05 s6=Singleton05.INSTANCE;
System.out.println(s5==s6); //输出为true }
}
【反射和序列化破解】
/***
* 使用反射和反序列破解4种单例模式(不包含枚举,枚举基于JVM底层无法破解,最安全)
* 以懒汉式为例
*/
package cn.sxt.pattern; import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Constructor; class Singleton06 implements Serializable{
private static Singleton06 s;
private Singleton06() {//加了抛出异常的私有化构造器,可以防止破解。当线程试图创建多个对象时会抛出异常。一般不用考虑
if (s!=null) {
throw new RuntimeException();
} }
/*private Singleton06() {//没加抛出异常的私有化构造器 }*/ //为啥加同步?因为当线程A执行到s==null后,睡觉去啦,当线程B进来后发现s也是null,会去创建一个对象,当线程A醒来之后也去创建
//一个新的对象,这样就2个对象,违反单例的定义(即一个单例类有且只有一个示例(对象))
public static synchronized Singleton06 getInstance() {
if (s==null) {
s=new Singleton06();
}
return s;
} //防止通过反序列破解单例。意思是在反序列化时直接调用这个方法,通过这个方法去返回我们指导的对象s,而不是创建一个新的对象
private Object readResolve() throws Exception {//这个方法自己不用调,实现反序列化时自动调用
return s;
}
} public class Test_0422_Singleton2 {
public static void main(String[] args) throws Exception {
Singleton06 s1=Singleton06.getInstance();
Singleton06 s2=Singleton06.getInstance();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1==s2); //输出为true,s1和s2是同一对象 /*//通过反射破解单例。通过反射直接调用私有构造器
Class<Singleton06> clz = (Class<Singleton06>)Class.forName("cn.sxt.pattern.Singleton06");
Constructor<Singleton06> constructor=clz.getDeclaredConstructor(null);
constructor.setAccessible(true);//跳过权限检测,访问私有对象
Singleton06 s3=constructor.newInstance();
Singleton06 s4=constructor.newInstance();
System.out.println(s3==s4);//输出为false,显然s3和s4不是同一对象。证明跳过了单例
*/ /**序列化:将对象状态转换为字节流的过程,可以将其保存到磁盘文件中或通过网络发送到任何其他程序;
* 反序列化:从字节流创建对象的相反的过程称为反序列化。而创建的字节流是与平台无关的,在一个平台上序列化的对象可以在
* 不同的平台上反序列化。
* */
//通过反序列化破解单例
FileOutputStream fos =new FileOutputStream("D:/a.txt");
ObjectOutputStream oos=new ObjectOutputStream(fos);
oos.writeObject(s1);//把对象s1经由ObjectOutputStream写出到文件 D:/a.txt中
oos.close();
fos.close(); ObjectInputStream ois=new ObjectInputStream(new FileInputStream("D:/a.txt"));
Singleton06 s5=(Singleton06)ois.readObject();
System.out.println(s5);//输出结果与s1和s2不同,证明破解了单例,若加了readResolve()方法,输出结果相同 }
}
【看看效率】
/***
* 测试5种单例模式的执行效率
* 懒汉式:效率最慢
*/
package cn.sxt.pattern; import java.util.concurrent.CountDownLatch; public class Test_0422_Singleton3 {
public static void main(String[] args) throws InterruptedException {
test(); } public static void test() throws InterruptedException {
int threadNum=5;
long start=System.currentTimeMillis(); /***解决时间不准的问题:主线程(与5个线程独立)可能已经执行完毕到end处了,输出主线程的时间。但是5个线程还有没有
* 执行完毕的,达不到效果。
* CountDownLatch类:同步辅助类,是一个统计是否线程结束计数器。 latch:插销 Down:向下
* -countDown() 当前线程调用此方法,则计数器减一
* -await() 调用此方法会一直阻塞线程,直至计数器为0
* */
final CountDownLatch count=new CountDownLatch(threadNum); for (int i = 0; i < threadNum; i++) {//创建5个独立线程,让每个线程去执行调用100次单例模式的getInstance()方法
new Thread(new Runnable() {
public void run() {
for (int j = 0; j < 10000; j++) {
//Object object=Singleton04.getInstance();
Object object2=Singleton05.INSTANCE;
}
count.countDown();//执行完一个线程 ,调用countDown()方法自动将threadNum(正在执行的线程总数)减一
}
}).start();
}
count.await();//阻塞main线程,一直等待,循环检测,看其他线程是否执行完才会继续往下 long end=System.currentTimeMillis();
System.out.println("总耗时:"+(end-start)+"毫秒");
}
}