likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 3

前言

初识享元模式(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 可以创建多个对象

总结

享元模式比较简单,主要用于减少创建对象数量,以减少内存占用和提高性能

使用享元模式要注意线程安全问题,个人认为线程不安全会造成重复创建对象,与享元模式减少创建对象数量的理念相悖


感谢阅读~不喜勿喷。欢迎讨论、指正