【问题标题】:Is a EJB Singleton synchronized?EJB Singleton 是否同步?
【发布时间】:2026-01-04 13:35:01
【问题描述】:

我已经实现了一种缓存 bean 来将数据对象缓存为 EJB 单例。我想知道这是否是 EJB 中的正确方法:

@Singleton
public class MyCache {

    int DEFAULT_CACHE_SIZE = 30;
    int DEFAULT_EXPIRES_TIME = 60000;
    long expiresTime = 0;
    long lastReset = 0;
    Cache cache = null; 

    ....
    @PostConstruct
    void init() {
        resetCache();
    }

    public void resetCache() {
        cache = new Cache(DEFAULT_CACHE_SIZE);
        lastReset = System.currentTimeMillis();
    }

    public void put(String key, Object value) {
        cache.put(key, value);
    }

    public Object get(String key) {
        // test if cache is expired
        if (expiresTime > 0) {
            Long now = System.currentTimeMillis();
            if ((now - lastReset) > expiresTime) {
                logger.finest("...... Cache expired!");
                resetCache();
            }
        }
        return cache.get(key);
    }



    class Cache extends LinkedHashMap<String, Object> implements Serializable {
        private static final long serialVersionUID = 1L;
        private final int capacity;

        public Cache(int capacity) {
            super(capacity + 1, 1.1f, true);
            this.capacity = capacity;
        }

        protected boolean removeEldestEntry(Entry<String, Object> eldest) {
            return size() > capacity;
        }
    }
}

我的问题是:这是实现应用程序范围缓存机制的正确方法吗?

我的印象是缓存的内容出乎意料地发生了变化。这会发生吗?例如,如果 EJB 被钝化了? 我在 Payara41 服务器中运行。

或者我必须使用:

cache = Collections.synchronizedMap(new Cache(DEFAULT_CACHE_SIZE));

代替:

cache = new Cache(DEFAULT_CACHE_SIZE);

【问题讨论】:

    标签: singleton ejb linkedhashmap


    【解决方案1】:

    首先,由于没有为您的 bean 指定并发管理,它归结为默认的“容器”。

    来自 EJB 3.1 规范:

    在设计单例会话 bean 时,开发人员必须决定 bean 将使用容器管理还是 bean 管理 并发。通常,单例 bean 将被指定为具有 容器管理的并发划分。如果没有,这是默认值 指定了并发管理类型。

    那么,容器并发管理需要锁类型的方法级规范。只要这些不存在,就会应用默认的“写入”:

    默认情况下,如果没有并发锁定属性注解 为受容器管理的 Singleton bean 的方法指定 并发分界,并发锁定的值 该方法的属性被定义为 Write。

    以上意味着对 bean 方法的访问必须是同步的,甚至可能比你实际需要的还要多。您可以为只读方法(get)设置“读取”锁定类型,以允许并发读取访问。

    【讨论】:

    • 感谢您的回复。我不知道默认行为。
    【解决方案2】:

    容器管理锁的解决方案有一个缺点, 考虑一下,如果你有一个带有 WRITE 锁的 put 操作,这意味着整个“缓存”都被阻塞了,所以无论 key 是否不同,都不能并行地 get 或 put。 如果你的缓存实现是一个并发 Map,你可以不加锁地使用它。

    如果您对缓存有更多要求,我会使用提供更好性能的 Infinispan。这里的缓存可以是本地的,也可以是分布在集群中的。

    【讨论】:

    • 感谢您的回复。我认为缺少的部分是使用并发地图。我会以这种方式改进我的代码。
    【解决方案3】:

    阅读此blog from Adam Bien 后,我现在通过以下方式改进了我的代码:

    • 我添加了注解 ConcurrencyManagement
    • 我将 LinkedHashMap 更改为 ConcurrentHashMap。

    例子:

    @Singleton
    @ConcurrencyManagement(ConcurrencyManagementType.BEAN) // added concurrency management
    public class MyCache {
    
        int DEFAULT_CACHE_SIZE = 30;
        int DEFAULT_EXPIRES_TIME = 60000;
        long expiresTime = 0;
        long lastReset = 0;
        Cache cache = null; 
    
        ....
        @PostConstruct
        void init() {
            resetCache();
        }
    
        public void resetCache() {
            cache = new Cache(DEFAULT_CACHE_SIZE);
            lastReset = System.currentTimeMillis();
        }
    
        public void put(String key, Object value) {
            cache.put(key, value);
        }
    
        public Object get(String key) {
            // test if cache is expired
            if (expiresTime > 0) {
                Long now = System.currentTimeMillis();
                if ((now - lastReset) > expiresTime) {
                    logger.finest("...... Cache expired!");
                    resetCache();
                }
            }
            return cache.get(key);
        }
    
        // changed from LinkedHashMap to ConcurrentHashMap
        class Cache extends ConcurrentHashMap<String, Object> implements Serializable {
            private static final long serialVersionUID = 1L;
            private final int capacity;
    
            public Cache(int capacity) {
                super(capacity + 1, 1.1f);
                this.capacity = capacity;
            }
    
            protected boolean removeEldestEntry(Entry<String, Object> eldest) {
                return size() > capacity;
            }
        }
    }
    

    我认为现在这是更正确的实现方式。

    【讨论】:

    • 我不认为这是一个正确的实现。您的 lastReset 值不是易失性的,因此不同的线程可能会看到不同的值,因为没有定义内存同步语义。 Adams 代码的另一个区别是,Adam 在@PostConstruct 期间创建了一次同步地图。您正在动态创建新实例。即使这不是易失性的,我认为缓存的创建也不是原子的,根本没有显式同步。
    最近更新 更多