设计模式-享元模式及应用

  在程序开发的过程中,往往会存在大量的重复的对象。重复的创建对象往往需要虚拟机开辟多块内存来存储这些对象,这就造成了内存的浪费。如果流量洪峰过来,而且对于系统的稳定性也是一个挑战。享元模式就可以解决我们所说的问题,“享元”何为享元,就是共享对象。重复的对象,无需重复创建,节约了内存空间。

享元的定义与特点

  享元模式的定义提出了两种边界的要求,一个是内部状态,一个是外部状态。

  • 内部状态指对象共享出来的信息,存储在享元信息内部,并且不回随环境的改变而改变;
  • 外部状态指对象得以依赖的一个标记,随环境的改变而改变,不可共享。
    后续的代码案例中,会有具体的体会。
享元模式的结构

享元模式的主要角色有如下。

  • 抽象享元角色(Flyweight):是所有的具体享元类的基类,为具体享元规范需要实现的公共接口,非享元的外部状态以参数的形式通过方法传入。
  • 具体享元(Concrete Flyweight)角色:实现抽象享元中的公共接口。
  • 非享元(Unsharable Flyweight)角色:是不可以共享的外部状态,它以参数的形式注入具体享元的相关方法中。
  • 享元工厂(Flyweight Factory)角色:享元对象的工厂,负责创建和管理享元角色。

享元模式结构图:
在这里插入图片描述

代码案例:
这里列举一个下期的案例,也是看网上举的比较典型的一个享元模式的案例。我们现在模拟下五子棋,然后棋子的种类只有两种,假如我们的棋盘是10x10的大小,如果设计下五子棋这个代码。 假如我们将棋子的颜色、坐标都包含在棋子的属性上。那么我们需要创建的棋子数目 2x10x10种可能。如果我们利用享元的设计思想,将相同的共享的角色棋子共享,是不是就只需要两个对象了,具体的坐标再抽离为不可共享的角色,使用时传入指定坐标。下面看下代码实现:

  • 抽象享元角色(棋子)
//棋子抽象
public interface ChessPieces {
	//下棋
	void down(Point point);
}
  • 具体享元角色1 黑棋
public class BlackChessPieces implements ChessPieces{
	//棋子颜色
	private String color;
	
	public void setKey(String color) {
		this.color = color;
	}

	@Override
	public void down(Point point) {
	System.out.println("当前黑棋子落盘位置 X:"+ point.getX() + ", Y:" + point.getY());
	}

}
  • 具体享元角色1 白棋
public class WhiteChessPieces implements ChessPieces {
	// 棋子颜色
	private String color;

	public void setKey(String color) {
		this.color = color;
	}

	@Override
	public void down(Point point) {
		System.out.println("当前黑棋子落盘位置 X:"+ point.getX() + ",Y:"+point.getY());
	}

}
  • 非享元角色享元角色中引用对象 坐标
public class Point {
	//横坐标
	private Integer x;
    //纵坐标
	private Integer y;

	public Point(Integer x, Integer y) {
		this.x = x;
		this.y = y;
	}

	public Integer getX() {
		return x;
	}

	public void setX(Integer x) {
		this.x = x;
	}

	public Integer getY() {
		return y;
	}

	public void setY(Integer y) {
		this.y = y;
	}
}
  • 享元工厂类
public class ChessPiecesFactory {
	
	//享元对象资源池
    private Map<String, ChessPieces> pool = new HashMap<>();

	public ChessPieces getChessPieces(String key){
		ChessPieces poolChessPieces = pool.get(key);
		if (poolChessPieces!=null) {
			System.out.println("成功获取" + key + "棋子");
			return poolChessPieces;
		}else {
			if (key.equals("黑色")) {
				BlackChessPieces blackChessPieces = new BlackChessPieces();
				blackChessPieces.setKey(key);
				pool.put(key, blackChessPieces);
				System.out.println("黑色棋子已被创建");
				return blackChessPieces;
			}else {
				WhiteChessPieces whiteChessPieces = new WhiteChessPieces();
				whiteChessPieces.setKey(key);
				pool.put(key, whiteChessPieces);
				System.out.println("白色棋子已被创建");
				return whiteChessPieces;
			}
		}
	}
	
	public int poolSize(){
		return pool.size();
	}
}
  • 使用方 模拟两人下棋
public class Client {
	public static void main(String[] args) {
		ChessPiecesFactory chessPiecesFactory = new ChessPiecesFactory();
		
		String black = "黑色";
		String white = "白色";
		//假如模拟两人下期 小A、小B
		//A:黑子先走
		ChessPieces chessPieces = chessPiecesFactory.getChessPieces(black);
		Point point1 = new Point(0, 0);
		chessPieces.down(point1);
		System.out.println("棋子数量:"+ chessPiecesFactory.poolSize());
		//B:白色
		ChessPieces chessPieces2 = chessPiecesFactory.getChessPieces(white);
		Point point2 = new Point(0, 1);
		chessPieces2.down(point2);
		System.out.println("棋子数量:"+ chessPiecesFactory.poolSize());
		//A:黑色
		ChessPieces chessPieces3 = chessPiecesFactory.getChessPieces(black);
		Point point3 = new Point(1, 1);
		chessPieces3.down(point3);
		System.out.println("棋子数量:"+ chessPiecesFactory.poolSize());
		
	}
}

每次获取棋子后,查看当前共享资源池中的对象数量.

执行结果:

当前黑棋子落盘位置 X:0, Y:0
棋子数量:1
白色棋子已被创建
当前黑棋子落盘位置 X:0,Y:1
棋子数量:2
成功获取黑色棋子
当前黑棋子落盘位置 X:1, Y:1
棋子数量:2

可以看到,当第二次我们下黑棋时候,棋的数量没有改变。

案例结构图:
在这里插入图片描述

享元模式的优缺点

优点:

  • 相同对象只要保存一份,这降低了系统中对象的数量,从而降低了系统中细粒度对象给内存带来的压力,减少了内存使用不足的风险。

缺点:

  • 为了使对象可以共享,需要将一些不能共享的状态外部化,这将增加程序的复杂性。
  • 内外的边界界定不好把控,如果后续更改引用处也需要修改,违背了开闭原则。
  • 享元模式的工厂类一直保存着享元对象,即使没有其他对象使用,垃圾回收器也不会去回收,会造成内存浪费。
享元模式的使用场景

系统设计的过程中,就需要考虑到这一点,如果一个对象频繁的创建,而且又有公共的部分能共享。并且创建的多个对象,可能会造成内存的压力,这时候需要考虑通过享元模式来优化。

  • 系统中存在大量相同相似对象;
  • 细粒度的对象都具备较为接近的外部状态,内部共享状态一致;
享元模式在JDK源码中的使用

1.享元模式在Integer中的使用
先看段代码:

public class Test {
	public static void main(String[] args) {
		Integer i1 = 88;
		Integer i2 = 88;
		
		Integer i3 = 188;
		Integer i4 = 188;		
		System.out.println(i1==i2);
		System.out.println(i3==i4);
	}
}

第一个输出的结果是true,第二个是false,为什么会出现这种情况,不妨点开Integer的源码,就能找到其原因;看源码之前先明确一个问题,基本数据类型JDK默认都有装箱开箱的操作,什么意思呢,就是如果是这我们使用到了基本类型定义变量,例如我们上面例子所写的Integer i1 = 88,JDK底层执行的其实是Integer i1 = Integer.valueOf(88);的操作,反过来如果变量i1包装类型通过基本数据类型int接收int i = i1,底层其实操作的是 int i = i1.intValue()。明确了这一点,我们看下Integer的源码:

public static Integer valueOf(String s, int radix) throws NumberFormatException {
        return Integer.valueOf(parseInt(s,radix));
    }
    
    
//  valueOf 方法
 public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }
    
//注意变量 IntegerCache
private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[];

        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;

            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);

            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }

        private IntegerCache() {}
    }

发现默认静态块中,随着类的加载,已经将[-128, 127]的数据放到Integer cache[]数组中。这就造成了我们案例中如果是这个范围中的数值,直接在cache中拿,如果超出界限,我们才会去创建新的Integer对象。我们可以看到,默认有最小的数,最大的数值赋值,给的127,我们也可以通过参数设置,来更改最大值,根据自己的需要。如源码中所示

String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); 

可以在启动参数上添加-Djava.lang.Integer.IntegerCache.high=255

2.享元模式在String中的使用
先看一段代码:

       String str1 = "a";	
		String str2 = "a";
		String str3 = new String("a");
		String str4 = new String("b") + new String("c");
		str4.intern();
		String str5 = "bc";
		System.out.println(str1 == str2); //true
		System.out.println(str1 == str3); //false
		System.out.println(str4==str5);//true

如果猜对了所有的结果,说明对String底层的存储了解已经到位。回到问题的本质,为什么str1 == str2结果为true。而str1 == str3结果为false。String str1 = "a"这种方式定义的字符串会放到JVM内存的常量池空间中。所以定义str2时,直接去引用常量池中已存在的字符串。
String str3 = new String("a");会在堆空间中开辟一块内存,Str3的地址指向是新开辟堆空间的地址,所有str1和Str3不是同一个对象。
String str4 = new String("b") + new String("c");String str5 = "bc";,为什么又相同了,这中间有个方法str4.intern();。此操作执行两步操作,判断常量池中是否有Str4的字符串,如果没有则把字符串的值存储在常量池中(JDK7,JDK6的方式不同,JDK6直接将字符串存储到常量池中,而JDK7中是把对象地址的引用存储在常量池中,当前案例是基于JDK8),所以在创建Str5时,直接就通过已存在的地址,指向同一个对象,所以比较的对象地址相同。str4==str5,结果为true。

总结:Java String 类的实现中,JVM开辟一块存储区专⻔存储字符串常量,字符串常量池,类似于Integer中的IntegerCache。不过,跟IntegerCache不同的是,它并非事先创建好需要共享的对象,而是 在程序的运行期间,根据需要来创建和缓存字符串常量。

拓展

1.享元模式与池化技术区别
网上看到很多文章,可以通过享元模式来实现线程池、数据库连接池等。享元模式中的共享是对同一个对象多处都可以共同使用,而我们所说的线程池、数据库连接池等技术,这里共享是线程不可共享的;比如数据库连接池,是初始化好一批数据库连接,某个线程去使用时,用完再归还。线程之间是不可共享的,而本文介绍的享元,共享部分就是对象相同。整个生命周期,都被所有的使用者共同持有。
那这里通过享元模式去实现线程池,数据库连接池。就需要考虑到这两者“共享区别”。

✨✨ 欢迎🔔订阅个人的微信公众号 享及时博文更新
个人工作号

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值