对象性能模式
面向对象很好地解决了“抽象”的问题,但是必不可免地要付出一定的代价。对于通常情况来讲,面向对象的成本大都可以忽略不计。但是某些情况,面向对象所带来的成本必须谨慎处理。
典型模式
Singleton(单例模式)
Flyweight(享元模式)
Singleton 单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
- 1、单例类只能有一个实例。
- 2、单例类必须自己创建自己的唯一实例。
- 3、单例类必须给所有其他对象提供这一实例。
意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决:一个全局使用的类频繁地创建与销毁。
何时使用:当您想控制实例数目,节省系统资源的时候。
如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
关键代码:构造函数是私有的。
应用实例:
- 1、一个班级只有一个班主任。
- 2、Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。
- 3、一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。
优点:
- 1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
- 2、避免对资源的多重占用(比如写文件操作)。
缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
使用场景:
- 1、要求生产唯一序列号。
- 2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
- 3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。
注意事项:getInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。
使用场景
在软件系统中,经常有这样一- 些特殊的类,必须保证它们在系统中只存在一一个实例,才能确保它们的逻辑正确性、以及良好的效率。
模式定义
保证一个类仅有一个实例,并提供一个该实例的全局访问点。
类图结构
代码举例
懒汉式 线程不安全
这个Singleton 只能使用在单线程情况下,如果出现多线程该Singleton 就不适用了。
比如s1和s2在分别在两个线程 使用 Singleton.getInstance()。
如果s1执行到 if (instance == null ) 没有执行 instance = new Singleton(); 如果此时s2也是执行到了 if (instance == null ) 这是可以通过的。然后s1,s2同时执行 instance = new Singleton(); 那么 Singleton 就会在系统中创建两次。并且有可能 两个instance是不同的对象。
public class Singleton {
private static Singleton instance;
private Singleton() { } //把构造器屏蔽掉 这样也就不会默认生成构造器了
//这样就不能使用new Singleton()创建对象了
//如果不写构造器 编译器默认会创建一个public空的构造器
public static Singleton getInstance() {
if (instance == null ) {
//Singleton 是 private 只能在类内部使用
instance = new Singleton();
}
return instance;
}
}
//使用者外面调用
// Singleton s1=Singleton.getInstance();
// Singleton s2=Singleton.getInstance();
// Singleton s3=Singleton.getInstance();
双检锁/双重校验锁
如果只使用同步代码块 synchronized (Singleton.class)解决多线程问题,表面上看似是解决了多线程的问题,但是还是会存在问题的。
同步块语句效率不高,比如10个线程同时执行getInstance(),这10个线程会在同步代码块进行等待。
所以使用双检锁,在同步代码块外面 添加 if (instance == null ) 判断。
双检锁也还是存在问题的,instance = new Singleton();即便做了同步操作,java编译器会对它进行reorder。在整个CPU执行的时候,是多步指令才能执行一行代码。
构造函数调用的时候有三个阶段。
1. 先分配内存
2. 执行构造器
3. 将内存地址复制给instance
instance = new Singleton();在CPU执行的时候是有多种可能性的
1.先分配内存,然后执行构造器,最好讲内存地址复制给instance (常规)
2.有可能先分配内存,然后就把内存地址复制给instance才会去执行构造器。
在代码中 private static Singleton instance; 添加 volatile 修饰符,编译器就不会对代码reorder了。
该版本可以,但是写代码比较多。
public class Singleton {
private static volatile Singleton instance;
private Singleton() { }
public static Singleton getInstance() {
if (instance == null ) { //如果instance不为空 就不需要同步
//添加同步语句
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
懒汉式 线程安全
如果多线程读取机会并不多,可以把 synchronized 加入方法上。整个方法做同步。
如果多个线程去读取的时候,同步性能比较差。
该版本不推荐使用
public class Singleton {
private static volatile Singleton instance = null;
private Singleton() { }
//基于线程不安全的懒汉式 添加 synchronized
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
饿汉式
可以保证线程安全,因为java static block 是确定只是单线程的,只有一个线程能进去,其他线程是无法进去的,java语言机制会保证这一点。不调用Singleton的时候也会创建。Singleton类ClassLoader被加载的时候,private static final Singleton instance = new Singleton();这个static block 就会被立即执行。执行比较早,如果不使用就比较浪费了。
该本版还凑合,只是没有使用懒加载
public class Singleton {
//直接初始化 instance
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
登记式/静态内部类
多线程版本,推荐使用这个模式,安全又简便。
//既实现了 懒加载 又实现了 多线程安全
public class Singleton {
private Singleton() { }
//lazy load
//内部嵌套类 外部是 没有办法直接访问 SingletonHolder类的
//所以外部的ClassLoader 不可能加载此类 因此instance = new Singleton(); 不会一开始就被执行
private static class SingletonHolder {
private static final Singleton instance = new Singleton();
}
//只有调用 getInstance() 才会加载 SingletonHolder类
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}
要点总结
Singleton模式中的实例构造器可以设置为protected以允许子类派生。
Singleton模式一般不要 支持拷贝构造函数和Clone接口,因为这有可能导致多个对象实例,与Singleton模式的初衷违背。
Flyweight 享元模式
享元模式(Flyweight Pattern)主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结构的方式。
享元模式尝试重用现有的同类对象,如果未找到匹配的对象,则创建新对象。
意图:运用共享技术有效地支持大量细粒度的对象。
主要解决:在有大量对象时,有可能会造成内存溢出,我们把其中共同的部分抽象出来,如果有相同的业务请求,直接返回在内存中已有的对象,避免重新创建。
何时使用: 1、系统中有大量对象。 2、这些对象消耗大量内存。 3、这些对象的状态大部分可以外部化。 4、这些对象可以按照内蕴状态分为很多组,当把外蕴对象从对象中剔除出来时,每一组对象都可以用一个对象来代替。 5、系统不依赖于这些对象身份,这些对象是不可分辨的。
如何解决:用唯一标识码判断,如果在内存中有,则返回这个唯一标识码所标识的对象。
关键代码:用 HashMap 存储这些对象。
应用实例: 1、JAVA 中的 String,如果有则返回,如果没有则创建一个字符串保存在字符串缓存池里面。 2、数据库的数据池。
优点:大大减少对象的创建,降低系统的内存,使效率提高。
缺点:提高了系统的复杂度,需要分离出外部状态和内部状态,而且外部状态具有固有化的性质,不应该随着内部状态的变化而变化,否则会造成系统的混乱。
使用场景: 1、系统有大量相似对象。 2、需要缓冲池的场景。
注意事项: 1、注意划分外部状态和内部状态,否则可能会引起线程安全问题。 2、这些类必须有一个工厂对象加以控制。
使用场景
在软件系统采用纯粹对象方案的问题在于大量细粒度的对象会很快充斥在系统中,从而带来很高的运行时代价——主要指内存需求方面的代价。
模式定义
运用共享技术有效地支持大量细粒度的对象。
类图结构
代码举例
比如创建一个文本处理的程序,一个字符对应一个字体对象。字符占用资源特别大,特别是内存。
用户一般会用到的字体应用可能不多,可能5-8种,不可能10W个字符用10W个对象。有可能只可能用到1000多个字体。
//比如32位的机器
//java必须做内存对齐,必须是4的倍数 4*5 =20byte 额外加上系统自带的byte 假设还有10个int字段 4* 10 = 40byte
//如果有5个指针指出去 那么就是 5 * 50 =250byte
// Font类对象大小 4, 1, 4, 4, 4=20byte+8byte=28byte+40byte= 68byte+ 250byte
public class Font {
private String key; //占4byte
private String name; //占4byte
private String decorator; //占4byte
private boolean isBold;//占1byte
private int size; //占4byte
public Font(String key){
//...
}
}
//一个对象常规的内存是 50byte ~ 300byte
// 1000 object= 50k~ 300k
// 10000 object= 500k~ 3M 如果某一类对象内存超过1M级别 就需要警惕
public class FontFactory{
private Map<String, Font> fonts = new ConcurrentHashMap<String, Font>();
public Font getFont(String key) {
if (!fonts.containsKey(key)) {
fonts.put(key, new Font(key));
}
return fonts.get(key);
}
}
class Client{
public static void main(String[] args) {
String s1="Hello,World";
String s2="Hello,World";
String s3=getString();
//如果值已经有了 直接去指向池里边的字符串即可 不用引用其他字符串 临时字符串可以更早的被垃圾回收器回收掉
s3=s3.intern();
}
public static String getString()
{
//...
return "";
}
}
类图结构中的 FlyweightFactory 对应代码中的 FontFactory,GetFlyweight() 对应代码中的 getFont()。
要点总结
面向对象很好地解决了抽象性的问题,但是作为一个运行在机器中的程序实体,我们需要考虑对象的代价问题。Flyweight主要解决面向对象的代价问题,一般不触及面向对象的抽象性问题。
Flyweight采用对象共享的做法来降低系统中对象的个数,从而降低细粒度对象给系统带来的内存压力。在具体实现方面,要注意对象状态的处理。