设计模式-单例模式

最近开发的时候遇到这样的需求,在数据源被加载进来之后会首先判断当前的数据源是否是可用的,在可用的情况下在执行其他的操作。判断的数据源是否是可用的自然就需要连接到数据源然后执行一些判断的逻辑。连接到数据源只需要使用java创建一个简单的连接就行,如果不重用已经创建的连接在大量用户使用的情况下会创建很多连接导致数据源不稳定从而影响业务方的正常使用。因此就结合guava实现了一个简单的连接缓存对象如下所示:

public abstract class BaseCacheManager<K, V> {

    /**
     * 最大的缓存的对象
     */
    private long guavaCacheSize = 100000;

    /**
     * 缓存存活的时间,默认值是10。具体的单位根据组件设置
     */
    private long guavaCacheTime = 10;

    /**
     * 全局的缓存的对象
     */
    private LoadingCache<K, V> globalCache = null;

    /**
     * 存放初次建立的连接
     */
    private Map<K,V> earlyConnectionMap = new ConcurrentHashMap<>();

    protected void setGuavaCacheSize(long guavaCacheSize) {
        this.guavaCacheSize = guavaCacheSize;
    }

    protected void setGuavaCacheTime(long guavaCacheTime) {
        this.guavaCacheTime = guavaCacheTime;
    }

    protected void init(TimeUnit timeUnit) {
        globalCache = loadCache(new CacheLoader<K, V>() {
            @Override
            public V load(K k) throws Exception {
                // 当k不存在的时候直接返回null
                return null;
            }
        },timeUnit, generateRemovalListener());
    }

    protected abstract RemovalListener<K, V> generateRemovalListener();

    private LoadingCache<K, V> loadCache(CacheLoader<K, V> cacheLoader, TimeUnit timeUnit,RemovalListener<K, V> removalListener) {
        LoadingCache<K, V> cache = CacheBuilder.newBuilder()
                //缓存池大小,在缓存项接近该大小时, Guava开始回收旧的缓存项
                .maximumSize(guavaCacheSize)
                //设置时间对象没有被读/写访问则对象从内存中删除(在另外的线程里面不定期维护)
                .expireAfterAccess(guavaCacheTime, timeUnit)
                // 设置缓存在写入之后 设定时间 后失效
                .expireAfterWrite(guavaCacheTime, timeUnit)
                .removalListener(removalListener)
                .recordStats()
                .build(cacheLoader);
        return cache;
    }

    public Boolean put(K k, V v) {
        try {
            globalCache.put(k, v);
        } catch (Exception e) {
            return false;
        }
        return true;
    }

    public Boolean putAll(Map<K, V> map) {
        try {
            globalCache.putAll(map);
        } catch (Exception e) {
            return false;
        }

        return true;
    }

    public V get(K k) {
        V v = null;
        try {
            v = globalCache.get(k);
        } catch (Exception e) {
            log.warn("获取的数据的key异常,异常信息 -> [{}]", e.getMessage());
        }
        return v;
    }

    public Boolean remove(K key) {
        try {
            globalCache.invalidate(key);
        } catch (Exception e) {
            log.error("移除缓存出错,异常信息 -> [{}]", e.getMessage());
            return false;
        }
        return true;
    }

    public Boolean removeAll(Iterable<K> keys) {
        try {
            globalCache.invalidateAll(keys);
        } catch (Exception e) {
            log.error("批量移除缓存出错,异常信息 -> [{}]", e.getMessage());
            return false;
        }
        return true;
    }


    public Boolean removeAll() {
        try {
            globalCache.invalidateAll();
        } catch (Exception e) {
            log.error("清空所有缓存出错,异常信息 -> [{}]", e.getMessage());
            return false;
        }
        return true;
    }


    public long size() {
        long size = 0;
        try {
            size = globalCache.size();
        } catch (Exception e) {
            log.error("获取缓存项数量出错,e -> [{}]", e.getMessage());
        }
        return size;
    }
}

当数据源是MySQL的时候实现如下:

public class MySQLConnectionCacheManager extends BaseCacheManager<String, Connection>{

    @Value("${mysql.cache.size}")
    private Long mysqlCacheManagerCacheSize;

    @Value("${mysql.cache.time}")
    private Long mysqlCacheManagerCacheTime;

    @PostConstruct
    public void initCacheManager(){
        if (mysqlCacheManagerCacheSize != null && mysqlCacheManagerCacheSize > 0){
            setGuavaCacheSize(mysqlCacheManagerCacheSize);
        }

        if (mysqlCacheManagerCacheTime != null && mysqlCacheManagerCacheTime > 0){
            setGuavaCacheTime(mysqlCacheManagerCacheSize);
        }

        init(TimeUnit.HOURS);
    }

    @Override
    protected RemovalListener<String, Connection> generateRemovalListener() {
        return removalNotification -> {
            try {
                Connection connection = removalNotification.getValue();
                if (connection != null && !connection.isClosed()){
                    connection.close();
                }
            }catch (SQLException sqlException){
                log.warn("关闭MySQL连接失败,异常信息 -> [{}]",sqlException.getCause());
            }
        };
    }
}

然后在项目获取的时候直接这样使用就行:

    /**
     * todo Connection 存在的线程安全问题。但是在这里是简单的select没有涉及到事务其内部实现了同步
     * @see StatementImpl#executeQuery(java.lang.String)
     * DCL创建新的连接
     */
    private Connection getConnByKeyIfNotExistsCreateIt(String uniqueKey, DataSourceInstance dataSource, String dbName,
                                                     String user, String pwd){
        Connection connection = null;
        connection = mySQLConnectionCacheManager.get(uniqueKey);
        try {
            if (connection == null){
                synchronized (this){
                    connection = mySQLConnectionCacheManager.get(uniqueKey);
                    if (connection == null){
                        connection = DriverManager.getConnection(String.format(jdbcUrl, dataSource.getHost(),
                                dataSource.getPort(),dbName),
                                user, pwd);
                        mySQLConnectionCacheManager.put(uniqueKey,connection);
                    }
                }
            }
        }catch (SQLException sqlException){
            log.error("创建连接失败,异常信息 -> [{}]",sqlException.getMessage());
            throw new RuntimeException("创建sql连接失败");
        }
        return connection;
    }

在获取连接的时候简单的使用了单例模式,也借此机会来重新梳理一下单例模式相关的知识。

  1. 什么是单例模式:在相同的前置条件的情况下,同一个资源只允许被实例化的一次。就像某个类是单例的,那么在这个类从jvm进程启动到jvm进程退出只会被创建一次。又像上面的链接信息一样,同一个MySQL实例的同一个账号同一个库表的链接信息也是只会被创建一次。
  2. 实现单例模式设计模式的几种方式
  • 饿汉式
public class Instance {
    private Instance(){}
    private static final Instance instance = new Instance();

    public static Instance getInstance() {
        return instance;
    }
}

这样的实现方式存在如下问题:不支持懒加载,会随着类的初始化而初始化。加入初始化的消耗的时间比较长或者需要的资源比较多会严重降低的整个项目的初始化的时候的性能。

  • 懒汉式
public class InstanceB {
    private static InstanceB instanceB;
    private InstanceB(){}

    public static synchronized InstanceB getInstance(){
        if (instanceB == null){
            instanceB = new InstanceB();
        }
        return instanceB;
    }
}

懒汉式的实现方式虽然解决了在初始化的时候消耗的资源的问题但是有带来的另外一个问题就是并发度低,每一次只有一个线程能进去获取的对象,其他的线程只能阻塞等待这个线程获取完成释放锁。

  • 双重检查锁
public class InstanceC {
    
    private InstanceC(){}
    
    private static volatile InstanceC instanceC;

    public static InstanceC getInstanceC() {
        if (instanceC == null){
            synchronized (InstanceC.class){
                if (instanceC == null){
                    instanceC = new InstanceC();
                }
            }
        }
        return instanceC;
    }
}

双重检查锁的方式不但解决了初始化的消耗资源的缺点,而且还提升了获取实例对象的并发度。
是不是有如下的疑问呢?
既然已经获取到锁了,为何还需要再一次判空?其实原因很简单,想想两个线程的情况:线程A和线程B都通过了第一个判断,但是线程A抢到了锁线程B阻塞等待。线程A执行完成释放锁,此时线程B获取到锁,加入不判断的这个实例会被线程B在创建一次。导致线程A和线程B引用的不是同一个对象。
为何需要使用volatile关键字的修饰instanceC变量呢?首先的从jvm到cpu层面的二进制执行单元都会对执行指令进行优化排序。new InstanceC()本身并不是的一个原子操作。当线程发生线程切换的时候instanceC还没有被初始化完成。导致其他线程获取到的对象是一个不完整的对象。volatile禁止指令重排序。保证对象的完整性。

  • 静态内部类
public class InstanceD {

    private InstanceD(){}

    private static class instanceDHolder{
        private static final InstanceD instanceD = new InstanceD();
    }

    public static InstanceD getInstance(){
        return instanceDHolder.instanceD;
    }
}

这样的实现方式和双重检查锁一样不但可以做到延迟加载还可以做到线程安全。那是如何做到延迟加载的呢?instanceDHolder是InstanceD的一个静态内部类,只有在getInstance的时候才会去触发这个类的加载。内部对象的instanceD的唯一性和安全性都是由jvm来保证的。

  • 枚举
public enum InstanceE {

    INSTANCE;
    private AtomicLong id = new AtomicLong(0);

    public long getId(){
        return id.getAndIncrement();
    }
}

到此为止的单例模式几种实现方式就已经总结完成。

参考链接:
王争老师设计模式之美

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值