前语

初识享元形式(Flyweight Pattern)的时分觉得没啥弯弯绕

属于结构型形式,是一种目标池技术,首要用于削减创立目标数量,以削减内存占用和进步功能

享,即同享;元,即目标

再看它的关键完成:用HashMap存储这些同享目标

很好理解嘛

形式初识

在某鸟教程中了解了享元形式

把它的比如简化下:“用2种颜色来画出分布于不同方位的圆”

(声明:本文不是说某鸟的比如不好啊,只是想经过该例渐进学习享元形式)

享元模式思考 - 线程安全问题

定义一个IShape并完成它

// 笼统享元人物(Flyweight)
public interface IShape {
    void draw();
}
// 具体享元(Concrete Flyweight)
public class Circle implements IShape {
    private String color;
    private int num, x, y;
    public Circle(String color) {
        this.color = color;
    }
    public void setNum(int num) {
        this.num = num;
    }
    public void setX(int x) {
        this.x = x;
    }
    public void setY(int y) {
        this.y = y;
    }
    @Override
    public void draw() {
        System.out.printf("画第%d个圆: %s色, [x, y] = [%d, %d]\n", num, color, x, y);
    }
}

创立享元工厂

public class ShapeFactory {
    private static Map<String, IShape> circleMap = new HashMap<>();
    public static IShape getCircle(String color) {
        Circle circle;
        if (circleMap.containsKey(color)) {
            circle = (Circle) circleMap.get(color);
        } else {
            circle = new Circle(color);
            System.out.println("*创立" + color + "色圆*");
            circleMap.put(color, circle);
        }
        return circle;
    }
}

OK,调用一下

public class FlyweightPatternDemo {
    public static void main(String[] args) {
        Circle circle1 = (Circle) ShapeFactory.getCircle("红");
        circle1.setNum(1);
        circle1.setX(1);
        circle1.setY(1);
        circle1.draw();
        Circle circle2 = (Circle) ShapeFactory.getCircle("绿");
        circle2.setNum(2);
        circle2.setX(2);
        circle2.setY(2);
        circle2.draw();
        // 复用
        Circle circle3 = (Circle) ShapeFactory.getCircle("红");
        circle3.setNum(3);
        circle3.setX(3);
        circle3.setY(3);
        circle3.draw();
    }
}

运转结果

享元模式思考 - 线程安全问题

内部状况 & 外部状况

能够看到Circle3的确没有创立新的circle目标,完成了复用

但感觉怪怪的

再一看

circle3circle1的序号(num)和方位(xy)也改了呀

享元模式思考 - 线程安全问题

本来想复制圆,整成了剪切圆

所以,不是所有的部分都能同享

享元形式的确也做了定义,它把一个目标的状况分为内部状况和外部状况

内部状况:不变的能够同享的部分
外部状况:随环境改变、不能同享的部分

想的还挺周到


改造下代码,把num、x、y抽取到新增的Location目标

public class Location {
    private int num, x, y;
    public Location(int num, int x, int y) {
        this.num = num;
        this.x = x;
        this.y = y;
    }
    public int getNum() {
        return num;
    }
    public int getX() {
        return x;
    }
    public int getY() {
        return y;
    }
}

改造IShapeCircle

public interface IShape {
    // 传入外部状况location
    void draw(Location location);
}
public class Circle implements IShape {
    private String color;
    public Circle(String color) {
        this.color = color;
    }
    @Override
    public void draw(Location location) {
        System.out.printf("画第%d个圆: %s色, [x, y] = [%d, %d]\n", location.getNum(), color, location.getX(), location.getY());
    }
}

OK,调用一下

public class FlyweightPatternDemo {
    public static void main(String[] args) {
        Circle circle1 = (Circle) ShapeFactory.getCircle("红");
        circle1.draw(new Location(1, 1, 1));
        Circle circle2 = (Circle) ShapeFactory.getCircle("绿");
        circle2.draw(new Location(2, 2, 2));
        // 复用
        Circle circle3 = (Circle) ShapeFactory.getCircle("红");
        circle3.draw(new Location(3, 3, 3));
    }
}

运转结果

享元模式思考 - 线程安全问题

线程安全问题

诶嘿,又发现了一个点

HashMap这玩意线程不安全啊

线程不安全的影响

用10个线程获取红色圆,测验上面经过HashMap完成的ShapeFactory

public class FlyweightPatternThreadDemo {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                Circle circle1 = (Circle) ShapeFactory.getCircle("红");
                Circle circle2 = (Circle) ShapeFactory.getCircle("红");
                System.out.println(circle1 == circle2);
            }).start();
        }
    }
}
// 输出:
// true
// true
// true
// true
// false  -- 阐明目标不一样了
// false  -- 阐明目标不一样了
// true
// false  -- 阐明目标不一样了
// false  -- 阐明目标不一样了
// true

因为线程不安全,所以会存在重复创立红色圆的状况

经过如下时序图阐明:

享元模式思考 - 线程安全问题

对策

Java中的String常量池、数据库连接池都运用了享元形式

咋保证的线程安全呢?

挑个了解的柿子捏下吧:Java String

哦~字符串常量池是一个固定大小的Hashtable

已然Hashtable能够那ConcurrentHashMap也能一战咯?

可是,把ShapeFactory里的circleMap完成换成这俩都不可

public class ShapeFactory {
    private static Map<String, IShape> circleMap = new ConcurrentHashMap<>();
    // private static Map<String, IShape> circleMap = new Hashtable<>();
    public static IShape getCircle(String color) {
        Circle circle;
        if (circleMap.containsKey(color)) {
            circle = (Circle) circleMap.get(color);
        } else {
            circle = new Circle(color);
            circleMap.put(color, circle);
        }
        return circle;
    }
}
// 输出:
// true
// true
// true
// true
// false  -- 阐明目标不一样了
// true
// true
// false  -- 阐明目标不一样了
// true
// true

因为containsKey()put()不是原子操作?

ConcurrentHashMap.putIfAbsent()是原子操作,整一个:

public class ShapeFactory {
    private static Map<String, IShape> circleMap = new ConcurrentHashMap<>();
    public static IShape getCircle(String color) {
        return circleMap.putIfAbsent(color, new Circle(color));
    }
}
// 输出:
// true
// true
// true
// true
// false -- 还是不可
// true
// true
// true
// true
// true

为啥还是不可呢?

因为 Thread1 和 Thread2 还是能够同时进ShapeFactory.getCircle()

享元模式思考 - 线程安全问题

只能给ShapeFactory.getCircle()加锁了

public class ShapeFactory {
    private static Map<String, IShape> circleMap = new ConcurrentHashMap<>();
    public static synchronized IShape getCircle(String color) {
        return circleMap.putIfAbsent(color, new Circle(color));
    }
}

这下总行了吧?运转下

OMG!还!是!不!行!

为啥呢?!!!

哈哈哈synchronized没毛病,问题出在ConcurrentHashMap.putIfAbsent()

putIfAbsent方法在向ConcurrentHashMap中增加键值对的时分,它会先判断该键值对是否现已存在
假如不存在(新的entry),那么会向map中增加该键值对,并返回null
假如已存在,那么不会覆盖已有的值,直接返回现已存在的值

这样改下就OK了

public class ShapeFactory {
    // getCircle()加锁了,那么HashMap、Hashtable也是能够的
    private static Map<String, IShape> circleMap = new ConcurrentHashMap<>();
    public static synchronized IShape getCircle(String color) {
        IShape circle = circleMap.putIfAbsent(color, new Circle(color));
        if (circle == null) {
            return circleMap.get(color);
        }
        return circle;
    }
}

与单例形式的差异

简略来说

单例形式:一个 class 只能有一个目标

享元形式:一个 class 能够创立多个目标

总结

享元形式比较简略,首要用于削减创立目标数量,以削减内存占用和进步功能

运用享元形式要注意线程安全问题,个人认为线程不安全会造成重复创立目标,与享元形式削减创立目标数量的理念相悖


感谢阅览~不喜勿喷。欢迎评论、纠正