1.4 原型模式
(1)概念
原型模式(Prototype Pattern)是用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。例如,一个对象需要在一个高代价的数据库操作之后被创建。我们可以缓存该对象,在下一个请求时返回它的克隆,在需要的时候更新数据库,以此来减少数据库调用。
(2)适用场景
何时使用:
1、当一个系统应该独立于它的产品创建,构成和表示时。
2、当要实例化的类是在运行时刻指定时,例如,通过动态装载。
3、为了避免创建一个与产品类层次平行的工厂类层次时。
4、当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。
**如何解决:**利用已有的一个原型对象,快速地生成和原型对象一样的实例。
关键代码:
1、实现克隆操作,在 JAVA 继承 Cloneable,重写 clone(),在 .NET 中可以使用 Object 类的 MemberwiseClone() 方法来实现对象的浅拷贝或通过序列化的方式来实现深拷贝。
2、原型模式同样用于隔离类对象的使用者和具体类型(易变类)之间的耦合关系,它同样要求这些"易变类"拥有稳定的接口。
应用实例: 1、细胞分裂。 2、JAVA 中的 Object clone() 方法。
(3)代码示例
创建一个实现了 Cloneable 接口的抽象类。
package com.alibaba.design.prototypepattern; /** * @author zhouyanxiang * @create 2020-07-2020/7/31-15:31 */ public abstract class Shape implements Cloneable { private String id; protected String type; abstract void draw(); public String getType(){ return type; } public String getId() { return id; } public void setId(String id) { this.id = id; } public Object clone() { Object clone = null; try { clone = super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return clone; } }
- Circle.java
package com.alibaba.design.prototypepattern; /** * @author zhouyanxiang * @create 2020-07-2020/7/31-15:34 */ public class Circle extends Shape { public Circle(){ type = "Circle"; } @Override public void draw() { System.out.println("Inside Circle::draw() method."); } }
- Rectangle.java
package com.alibaba.design.prototypepattern; /** * @author zhouyanxiang * @create 2020-07-2020/7/31-15:32 */ public class Rectangle extends Shape { public Rectangle(){ type = "Rectangle"; } @Override void draw() { System.out.println("Inside Rectangle::draw() method."); } }
- Square.java
package com.alibaba.design.prototypepattern; /** * @author zhouyanxiang * @create 2020-07-2020/7/31-15:33 */ public class Square extends Shape { public Square() { type = "Square"; } @Override void draw() { System.out.println("Inside Square::draw() method."); } }
创建一个类,从数据库获取实体类,并把它们存储在一个 Hashtable 中。
package com.alibaba.design.prototypepattern; import java.util.Hashtable; /** * @author zhouyanxiang * @create 2020-07-2020/7/31-15:35 */ public class ShapeCache { private static Hashtable<String, Shape> shapeMap = new Hashtable<String, Shape>(); public static Shape getShape(String shapeId) { Shape cachedShape = shapeMap.get(shapeId); return (Shape) cachedShape.clone(); } // 对每种形状都运行数据库查询,并创建该形状 // shapeMap.put(shapeKey, shape); // 例如,我们要添加三种形状 public static void loadCache() { Circle circle = new Circle(); circle.setId("1"); shapeMap.put(circle.getId(),circle); Square square = new Square(); square.setId("2"); shapeMap.put(square.getId(),square); Rectangle rectangle = new Rectangle(); rectangle.setId("3"); shapeMap.put(rectangle.getId(),rectangle); } }
PrototypePatternDemo 使用 ShapeCache 类来获取存储在 Hashtable 中的形状的克隆。
package com.alibaba.design.prototypepattern; /** * @author zhouyanxiang * @create 2020-07-2020/7/31-15:37 */ public class PrototypePatternDemo { public static void main(String[] args) { ShapeCache.loadCache(); Shape clonedShape = (Shape) ShapeCache.getShape("1"); System.out.println("Shape : " + clonedShape.getType()); Shape clonedShape2 = (Shape) ShapeCache.getShape("2"); System.out.println("Shape : " + clonedShape2.getType()); Shape clonedShape3 = (Shape) ShapeCache.getShape("3"); System.out.println("Shape : " + clonedShape3.getType()); } }
(4)该模式在源码中的体现
我们看一下Object这个对象,我们直接看一下克隆这个方法 protected native Object clone() throws CloneNotSupportedException; 很明显看出来,他是一个native的方法,接下来我们再看一个,Cloneable这个接口,public interface Cloneable, 只要看看哪些类实现这个接口呢,就知道了这个原型模式,是如何使用的,我们按一下Ctrl+T,这里面有一个定位, 直接点一下,会在下边显示出来,这里面开始搜索,在搜索的时候呢,我们再看一个类,ArrayList,相信这个类大家都是知道的, public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable 他实现了Cloneable这个接口,我们看一下它是如何重写的呢, public Object clone() { try { ArrayList<?> v = (ArrayList<?>) super.clone(); v.elementData = Arrays.copyOf(elementData, size); v.modCount = 0; return v; } catch (CloneNotSupportedException e) { // this shouldn't happen, since we are Cloneable throw new InternalError(e); } } 通过Arrays.copyOf这个方法把这里面的元素,copy了一份,同理HashMap这里面 public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable 是一样的,他也实现了Cloneable这个接口,同时他也重写了克隆这个方法 @SuppressWarnings("unchecked") @Override public Object clone() { HashMap<K,V> result; try { result = (HashMap<K,V>)super.clone(); } catch (CloneNotSupportedException e) { // this shouldn't happen, since we are Cloneable throw new InternalError(e); } result.reinitialize(); result.putMapEntries(this, false); return result; } // These methods are also used when serializing HashSets final float loadFactor() { return loadFactor; } final int capacity() { return (table != null) ? table.length : (threshold > 0) ? threshold : DEFAULT_INITIAL_CAPACITY; } 有兴趣的可以来看一下,那在这里想说一下,就是对于原型模式,我们平时在使用的时候,一定要检对象是否和预期是一致的, 也就是说这个对象,是新创建出来的呢,还是只是创建一个引用,指向的是同一个地址,也就是说要把深克隆和浅克隆一定要 应用好,我们看一下下面搜索出来的都是实现了Cloneable这个接口的实现类,这里又很多,这个是JDK的
包括redission,还有redis的client,还有Spring里面的mybatis,这里面都是实现了Cloneable接口,我们看一下Cache, CacheKey,这个类也实现了Cloneable, public class CacheKey implements Cloneable CacheKey是Mybatis里面关于Cache使用的一个类,我们看一下它是如何重写的 @Override public CacheKey clone() throws CloneNotSupportedException { CacheKey clonedCacheKey = (CacheKey) super.clone(); clonedCacheKey.updateList = new ArrayList<Object>(updateList); return clonedCacheKey; } 这里面可以看到,首先是super.clone(),然后强转一个新的出来,然后把里面的List赋值成一个新的List,并把这个元素放到 新的List里面,进行返回,这样就保证了ArrayList,是一个新创建的ArrayList,但是注意里面的updateList private List<Object> updateList; 这个又是一个List,有兴趣的,建议可以试一下,克隆出来的里面的,这个元素,是相同的对象还是不同的对象,非常你们能够操作一下, 你可以模拟CacheKey来写一个类,主要是List,然后再进一步的说,如果List里面包装的,不是String,而是Date对象,我们再看一下效果, 希望能够动起手来,那接着看,下面这些是实现Cloneable接口的,包括在开源框架中,会有很多,有兴趣的可以自己挨个看一下,这里面 还是挺有意思的,那原型模式在开源框架中,使用的非常广泛,就拿CacheKey来说,这里面也是非常注重克隆出来的对象,引用问题, 所以我们在实现原型模式的时候,这一点是一定一定要注意的.
(5)原型模式的优缺点
- 优点:
1、性能提高。
2、逃避构造函数的约束。 - 缺点:
1、配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类不是很难,但对于已有的类不一定很容易,特别当一个类引用不支持串行化的间接对象,或者引用含有循环结构的时候。
2、必须实现 Cloneable 接口。
1.5 对象池模式
(1) 概念
Object Pool,即对象池,对象被预先创建并初始化后放入对象池中,对象提供者就能利用已有的对象来处理请求,减少对象频繁创建所占用的内存空间和初始化时间,例如数据库连接对象基本上都是创建后就被放入连接池中,后续的查询请求使用的是连接池中的对象,从而加快了查询速度。类似被放入对象池中的对象还包括Socket对象、线程对象和绘图对象(GDI对象)等。
在Object Pool设计模式中,主要有两个参与者:对象池的管理者和对象池的用户,用户从管理者那里获取对象,对象池对于用户来讲是透明的,但是用户必须遵守这些对象的使用规则,使用完对象后必须归还或者关闭对象,例如数据库连接对象使用完后必须关闭,否则该对象就会被一直占用着。
对象管理者需要维护对象池,包括初始化对象池、扩充对象池的大小、重置归还对象的状态等。
对象池在被初始化时,可能只有几个对象,甚至没有对象,按需创建对象能节省资源和时间,对于响应时间要求较高的情况,可以预先创建若干个对象。
当对象池中没有对象可供使用时,管理者一般需要使用某种策略来扩充对象池,比如将对象池的大小翻倍。另外,在多线程的情况下,可以让请求资源的线程等待,直到其他线程归还了占用的对象。
一般来说,对象池中的对象在逻辑状态上是相同的,如果都是无状态对象(即没有成员变量的对象),那么这些对象的管理会方便的多,否则,对象被使用后的状态重置工作就要由管理者来承担
(2)适用场景
对象池模式经常用在频繁创建、销毁对象(并且对象创建、销毁开销很大)的场景,比如数据库连接池、线程池、任务队列池等。
(3)代码示例
示例的整体类图:
- Connection接口
package com.alibaba.design.objectpoolpattern; /** * @author zhouyanxiang * @create 2020-07-2020/7/31-19:14 */ public interface Connection { Object get(); void set(Object x); }
- 实现接口的连接类ConnectionImplementation
package com.alibaba.design.objectpoolpattern; /** * @author zhouyanxiang * @create 2020-07-2020/7/31-19:14 */ public class ConnectionImplementation implements Connection { @Override public Object get() { return new Object(); } @Override public void set(Object x) { System.out.println("设置连接线程为: " + x); } }
连接池类ConnectionPool
package com.alibaba.design.objectpoolpattern; /** * @author zhouyanxiang * @create 2020-07-2020/7/31-19:15 */ public class ConnectionPool { //池管理对象 private static PoolManager pool = new PoolManager(); //指定连接数 并添加 public static void addConnections(int number) { for (int i = 0; i < number; i++) { pool.add(new ConnectionImplementation()); System.out.println("添加第 " + i + " 个连接资源"); } } //获取连接 public static Connection getConnection() throws PoolManager.EmptyPoolException { return (Connection) pool.get(); } //释放指定的连接 public static void releaseConnection(Connection c) { pool.release(c); System.out.println("释放整个连接资源: " + c); } }
连接池管理类PoolManager
package com.alibaba.design.objectpoolpattern; import java.util.ArrayList; /** * @author zhouyanxiang * @create 2020-07-2020/7/31-19:12 */ public class PoolManager { //连接池对象 public static class PoolItem { boolean inUse = false; Object item; //池数据 PoolItem(Object item) { this.item = item; } } //连接池集合 private ArrayList items = new ArrayList(); public void add(Object item) { this.items.add(new PoolItem(item)); } static class EmptyPoolException extends Exception { } public Object get() throws EmptyPoolException { for (int i = 0; i < items.size(); i++) { PoolItem pitem = (PoolItem) items.get(i); if (pitem.inUse == false) { pitem.inUse = true; System.out.println("获取连接资源为: " + items.get(i)); return pitem.item; } } throw new EmptyPoolException(); // return null; } /** * 释放连接 * @param item */ public void release(Object item) { for (int i = 0; i < items.size(); i++) { PoolItem pitem = (PoolItem) items.get(i); if (item == pitem.item) { pitem.inUse = false; System.out.println("释放连接资源 : " + items.get(i)); return; } } throw new RuntimeException(item + " not null"); } }
客户端测试类
package com.alibaba.design.objectpoolpattern; /** * @author zhouyanxiang * @create 2020-07-2020/7/31-19:16 */ public class Test { static { ConnectionPool.addConnections(5); } public void test1() { Connection c; try { // 获得连接 c = ConnectionPool.getConnection(); } catch (PoolManager.EmptyPoolException e) { throw new RuntimeException(e); } // 设值 c.set(new Object()); //获取 c.get(); // 释放 ConnectionPool.releaseConnection(c); } public static void main(String args[]) { Test test = new Test(); test.test1(); } }
输出结果
(4)该模式在源码中的体现
javax.sql.PooledConnection
看它的注释
/** * An object that provides hooks for connection pool management. * A <code>PooledConnection</code> object * represents a physical connection to a data source. The connection * can be recycled rather than being closed when an application is * finished with it, thus reducing the number of connections that * need to be made. * <P> * An application programmer does not use the <code>PooledConnection</code> * interface directly; rather, it is used by a middle tier infrastructure * that manages the pooling of connections. * <P> * When an application calls the method <code>DataSource.getConnection</code>, * it gets back a <code>Connection</code> object. If connection pooling is * being done, that <code>Connection</code> object is actually a handle to * a <code>PooledConnection</code> object, which is a physical connection. * <P> * The connection pool manager, typically the application server, maintains * a pool of <code>PooledConnection</code> objects. If there is a * <code>PooledConnection</code> object available in the pool, the * connection pool manager returns a <code>Connection</code> object that * is a handle to that physical connection. * If no <code>PooledConnection</code> object is available, the * connection pool manager calls the <code>ConnectionPoolDataSource</code> * method <code>getPoolConnection</code> to create a new physical connection. The * JDBC driver implementing <code>ConnectionPoolDataSource</code> creates a * new <code>PooledConnection</code> object and returns a handle to it. * <P> * When an application closes a connection, it calls the <code>Connection</code> * method <code>close</code>. When connection pooling is being done, * the connection pool manager is notified because it has registered itself as * a <code>ConnectionEventListener</code> object using the * <code>ConnectionPool</code> method <code>addConnectionEventListener</code>. * The connection pool manager deactivates the handle to * the <code>PooledConnection</code> object and returns the * <code>PooledConnection</code> object to the pool of connections so that * it can be used again. Thus, when an application closes its connection, * the underlying physical connection is recycled rather than being closed. * <P> * The physical connection is not closed until the connection pool manager * calls the <code>PooledConnection</code> method <code>close</code>. * This method is generally called to have an orderly shutdown of the server or * if a fatal error has made the connection unusable. * * <p> * A connection pool manager is often also a statement pool manager, maintaining * a pool of <code>PreparedStatement</code> objects. * When an application closes a prepared statement, it calls the * <code>PreparedStatement</code> * method <code>close</code>. When <code>Statement</code> pooling is being done, * the pool manager is notified because it has registered itself as * a <code>StatementEventListener</code> object using the * <code>ConnectionPool</code> method <code>addStatementEventListener</code>. * Thus, when an application closes its <code>PreparedStatement</code>, * the underlying prepared statement is recycled rather than being closed. * <P> * * @since 1.4 */
/ ** *提供用于连接池管理的挂钩的对象。 * <code> PooledConnection </ code>对象 *表示与数据源的物理连接。连接 *可以回收而不是在应用程序关闭时关闭 *完成后,减少了连接数 *必须填写。 * <P> *应用程序程序员不使用<code> PooledConnection </ code> *直接接口;而是由中间层基础架构使用 *管理连接池。 * <P> *当应用程序调用方法<code> DataSource.getConnection </ code>时, *它返回一个<code> Connection </ code>对象。如果连接池是 *完成后,该<code> Connection </ code>对象实际上是 * <code> PooledConnection </ code>对象,它是物理连接。 * <P> *连接池管理器(通常是应用程序服务器)维护 * <code> PooledConnection </ code>对象的池。如果有 * <code> PooledConnection </ code>对象在池中可用, *连接池管理器返回一个<code> Connection </ code>对象,该对象 *是该物理连接的句柄。 *如果没有<code> PooledConnection </ code>对象可用,则 *连接池管理器调用<code> ConnectionPoolDataSource </ code> *方法<code> getPoolConnection </ code>创建一个新的物理连接。的 *实现<code> ConnectionPoolDataSource </ code>的JDBC驱动程序创建了一个 *新的<code> PooledConnection </ code>对象并返回其句柄。 * <P> *当应用程序关闭连接时,它会调用<code> Connection </ code> *方法<code> close </ code>。完成连接池后, *连接池管理器已被通知,因为它已将自己注册为 *使用<code> ConnectionEventListener </ code>对象 * <code> ConnectionPool </ code>方法<code> addConnectionEventListener </ code>。 *连接池管理器取消激活 * <code> PooledConnection </ code>对象并返回 * <code> PooledConnection </ code>对象到连接池,以便 *它可以再次使用。因此,当应用程序关闭其连接时, *基础物理连接被回收而不是关闭。 * <P> *直到连接池管理器才关闭物理连接 *调用<code> PooledConnection </ code>方法<code> close </ code>。 *此方法通常被称为有序关闭服务器或 *如果发生致命错误使连接无法使用。 * * <p> *连接池管理器通常也是语句池管理器,用于维护 * <code> PreparedStatement </ code>对象的池。 *当应用程序关闭准备好的语句时,它将调用 * <code> PreparedStatement </ code> *方法<code> close </ code>。完成<code> Statement </ code>池后, *通知池管理器,因为它已将自己注册为 *使用<code> StatementEventListener </ code>对象 * <code> ConnectionPool </ code>方法<code> addStatementEventListener </ code>。 *因此,当应用程序关闭其<code> PreparedStatement </ code>时, *基础的准备好的语句被回收而不是关闭。 * <P> * * @从1.4开始 * /
(5)对象池模式的优缺点
- 优点
复用池中对象,没有分配内存和创建堆中对象的开销, 没有释放内存和销毁堆中对象的开销, 进而减少垃圾收集器的负担, 避免内存抖动;不必重复初始化对象状态, 对于比较耗时的constructor和finalize来说非常合适; - 缺点
- Java的对象分配操作不比c语言的malloc调用慢, 对于轻中量级的对象, 分配/释放对象的开销可以忽略不计;
- 并发环境中, 多个线程可能(同时)需要获取池中对象, 进而需要在堆数据结构上进行同步或者因为锁竞争而产生阻塞, 这种开销要比创建销毁对象的开销高数百倍;
- 由于池中对象的数量有限, 势必成为一个可伸缩性瓶颈;
- 很难正确的设定对象池的大小, 如果太小则起不到作用, 如果过大, 则占用内存资源高, 可以起一个线程定期扫描分析, 将池压缩到一个合适的尺寸以节约内存,但为了获得不错的分析结果, 在扫描期间可能需要暂停复用以避免干扰(造成效率低下), 或者使用非常复杂的算法策略(增加维护难度);
- 设计和使用对象池容易出错, 设计上需要注意状态同步, 这是个难点, 使用上可能存在忘记归还(就像c语言编程忘记free一样), 重复归还(可能需要做个循环判断一下是否池中存在此对象, 这也是个开销), 归还后仍旧使用对象(可能造成多个线程并发使用一个对象的情况)等问题