zoukankan      html  css  js  c++  java
  • 将ReadWriteLock应用于缓存设计

     在JavaEEdev站点(http://www.javaeedev.com )的设计中,有几类数据是极少变化的,如ArticleCategory(文档分类),ResourceCategory(资源分类),Board(论坛版面)。在对应的DAO实现中,总是一次性取出所有的数据,例如:

    List<ArticleCategory>  getArticleCategories();
    

      此类数据的特点是:数据量很小,读取非常频繁,变化却极慢(几天甚至几十天才变化一次),如果每次通过DAO从数据库获取数据,则增加了数据库 服务器的压力。为了在不影响整个系统结构的情况下透明地缓存这些数据,可以在Facade一层通过Proxy模式配合ReadWriteLock实现缓 存,而客户端和后台的DAO数据访问对象都不受影响:

    将ReadWriteLock应用于缓存设计

      首先,现有的中间层是由Facade接口和一个FacadeImpl具体实现构成的。对ArticleCategory的相关操作在FacadeImpl中实现如下:

    public class FacadeImpl implements Facade {
        protected CategoryDao categoryDao;
        public void setCategoryDao(CategoryDao categoryDao) {
            this.categoryDao = categoryDao;
        }
        // 读操作:
        public ArticleCategory queryArticleCategory(Serializable id) {
            return categoryDao.queryArticleCategory(id);
        }
        // 读操作:
        public List<ArticleCategory> queryArticleCategories() {
            return categoryDao.queryArticleCategories();
        }
        // 写操作:
        public void createArticleCategory(ArticleCategory category) {
            categoryDao.create(category);
        }
        // 写操作:
        public void deleteArticleCategory(ArticleCategory category) {
            categoryDao.delete(category);
        }
        // 写操作:
        public void updateArticleCategory(ArticleCategory category) {
            categoryDao.update(category);
        }
        // 其他方法省略...
    }
    

      设计代理类FacadeCacheProxy,让其实现缓存ArticleCategory的功能:

    public class FacadeCacheProxy implements Facade {
        private Facade target;
        public void setFacadeTarget(Facade target) {
            this.target = target;
        }
    
        // 定义缓存对象:
        private FullCache<ArticleCategory> cache = new FullCache<ArticleCategory>() {
            // how to get real data when cache is unavailable:
            protected List<ArticleCategory> doGetList() {
                return target.queryArticleCategories();
            }
        };
    
        // 从缓存返回数据:
        public List<ArticleCategory> queryArticleCategories() {
            return cache.getCachedList();
        }
    
        // 创建新的ArticleCategory后,让缓存失效:
        public void createArticleCategory(ArticleCategory category) {
            target.createArticleCategory(category);
            cache.clearCache();
        }
    
        // 更新某个ArticleCategory后,让缓存失效:
        public void updateArticleCategory(ArticleCategory category) {
            target.updateArticleCategory(category);
            cache.clearCache();
        }
    
        // 删除某个ArticleCategory后,让缓存失效:
        public void deleteArticleCategory(ArticleCategory category) {
            target.deleteArticleCategory(category);
            cache.clearCache();
        }
    }
    

      该代理类的核心是调用读方法getArticleCategories()时,直接从缓存对象FullCache中返回结果,当调用写方法(create,update和delete)时,除了调用target对象的相应方法外,再将缓存对象清空。

      FullCache便是实现缓存的关键类。为了实现强类型的缓存,采用泛型实现FullCache:

    public abstract class FullCache<T extends AbstractId> {
        ...
    }
    

      AbstractId是所有Domain Object的超类,目的是提供一个String类型的主键,同时便于在Hibernate或其他ORM框架中只需要配置一次JPA注解:

    @MappedSuperclass
    public abstract class AbstractId {
        protected String id;
    
        @Id
        @Column(nullable=false, updatable=false, length=32)
        @GeneratedValue(generator="system-uuid")
        @GenericGenerator(name="system-uuid", strategy="uuid")
        public String getId() { return id; }
        public void setId(String id) { this.id = id; }
    }
    

      FullCache实现以下2个功能:

    1. List<T> getCachedList():获取整个缓存的List<T>
    2. clearCache():清除所有缓存

      此外,FullCache在缓存失效的情况下,必须从真正的数据源获得数据,因此,抽象方法:

      protected abstract List<T> doGetList()

      负责获取真正的数据。

      下面,用ReadWriteLock实现该缓存模型。

      Java 5平台新增了java.util.concurrent包,该包包含了许多非常有用的多线程应用类,例如ReadWriteLock,这使得开发人员不必自己封装就可以直接使用这些健壮的多线程类。

      ReadWriteLock是一种常见的多线程设计模式。当多个线程同时访问同一资源时,通常,并行读取是允许的,但是,任一时刻只允许最多一个线程写入,从而保证了读写操作的完整性。下图很好地说明了ReadWriteLock的读写并发模型:

     

    允许不允许
    不允许不允许

     

     

     

     

          当读线程远多于写线程时,使用ReadWriteLock来取代synchronized同步会显著地提高性能,因为大多数时候,并发的多个读线程不需要等待。

      Java 5的ReadWriteLock接口仅定义了如何获取ReadLock和WriteLock的方法,对于具体的ReadWriteLock的实现模式并没 有规定,例如,Read优先还是Write优先,是否允许在等待写锁的时候获取读锁,是否允许将一个写锁降级为读锁,等等。

      Java 5自身提供的一个ReadWriteLock的实现是ReentrantReadWriteLock,该ReadWriteLock实现能满足绝大多数的多线程环境,有如下特点:

    1. 支持两种优先级模式,以时间顺序获取锁和以读、写交替优先获取锁的模式;
    2. 当获得了读锁或写锁后,还可重复获取读锁或写锁,即ReentrantLock;
    3. 获得写锁后还可获得读锁,但获得读锁后不可获得写锁;
    4. 支持将写锁降级为读锁,但反之不行;
    5. 支持在等待锁的过程中中断;
    6. 对写锁支持Condition(用于取代wait,notify和notifyAll);
    7. 支持锁的状态检测,但仅仅用于监控系统状态而并非同步控制;

      FullCache采用ReentrantReadWriteLock实现读写同步:

    public abstract class FullCache<T extends AbstractId> {
        private final ReadWriteLock lock = new ReentrantReadWriteLock();
        private final Lock readLock = lock.readLock(); // 读锁
        private final Lock writeLock = lock.writeLock(); // 写锁
    
        private List<T> cachedList = null; // 持有缓存的数据,若为null,表示缓存失效
    }
    

      对于clearCache()方法,由于其是一个写操作,故定义如下:

    public void clearCache() {
        writeLock.lock();
        cachedList = null;
        writeLock.unlock();
    }
    

      对于get方法,由于是读操作,同时要考虑在缓存失效的情况下更新数据,其实现就稍微复杂一点:

    public List<T> getCachedList() {
        // 获得读锁:
        readLock.lock();
        try {
            if(cachedList==null) {
                // 在获得写锁前,必须先释放读锁:
                readLock.unlock();
                writeLock.lock();
                try {
                    cachedList = doGetList(); // 获取真正的数据
                }
                finally {
                    // 在释放写锁前,先获得读锁:
                    readLock.lock();
                    writeLock.unlock();
                }
            }
            return cachedList;
        }
        finally {
            // 确保读锁在方法返回前被释放:
            readLock.unlock();
        }
    }
    

      通过适当的装配(例如在Spring IoC容器中),让客户端持有FacadeCacheProxy的引用,就在中间层完全实现了透明的缓存,客户端代码一行也不用更改。

      考虑到多线程模型远比单线程复杂,为了确保FullCache实现的健壮性,编写一个FullCacheTest来执行单元测试:

    public class FullCacheTest {
        // count how many hits:
        class Hit {
            private AtomicInteger total = new AtomicInteger(0);
            private AtomicInteger notHit = new AtomicInteger(0);
    
            public void total() {
                total.incrementAndGet();
            }
    
            public void notHit() {
                notHit.incrementAndGet();
            }
    
            public void debug() {
                System.err.println("Total get: " + total.intValue());
                System.err.println("Not hit: " + notHit.intValue());
                System.err.println("Hits: " + ((total.intValue()-notHit.intValue()) * 100 / total.intValue()) + "%");
            }
        }
    
        private static final int DATA_OPERATION = 10;
        private static final int MAX = 10;
        private static String[] ids = new String[MAX];
    
        static {
            for(int i=0; i<MAX; i++) {
                ids[i] = UUID.randomUUID().toString();
            }
        }
    
        private Hit hit;
        private FullCache<ArticleCategory> cache;
    
        @Before
        public void setUp() {
            hit = new Hit();
            cache = new FullCache<ArticleCategory>() {
                @Override
                protected List<ArticleCategory> doGetList() {
                    hit.notHit();
                    List<ArticleCategory> list = new ArrayList<ArticleCategory>();
                    for(int i=0; i<MAX; i++) {
                        ArticleCategory obj = new ArticleCategory();
                        obj.setId(ids[i]);
                        list.add(obj);
                    }
                    doSleep(DATA_OPERATION);
                    return list;
                }
    
                @Override
                public List<ArticleCategory> getCachedList() {
                    hit.total();
                    return super.getCachedList();
                }
            };
        }
    
        @Test
        public void testMultiThread() {
            final int THREADS = 100;
            final int LOOP_PER_THREAD = 100000;
            List<Thread> threads = new ArrayList<Thread>(THREADS);
            // test FullCache.getCachedList(id):
            for(int i=0; i<THREADS; i++) {
                threads.add(
                        new Thread() {
                            public void run() {
                                for(int j=0; j<LOOP_PER_THREAD; j++) {
                                    List<ArticleCategory> list = cache.getCachedList();
                                    for(int k=0; k<MAX; k++) {
                                        assertEquals(ids[k], list.get(k).getId());
                                    }
                                }
                            }
                        }
                );
            }
            // test FullCache.clearCache():
            Thread clearThread = new Thread() {
                    public void run() {
                        for(;;) {
                            cache.clearCache();
                            try {
                                Thread.sleep(DATA_OPERATION * 2);
                            }
                            catch(InterruptedException e) {
                                break;
                            }
                        }
                    }
            };
            // start all threads:
            clearThread.start();
            for(Thread t : threads) {
                t.start();
            }
            // wait for all threads:
            for(Thread t : threads) {
                try {
                    t.join();
                }
                catch(InterruptedException e) {}
            }
            clearThread.interrupt();
            try {
                clearThread.join();
            }
            catch(InterruptedException e) {}
            // statistics:
            hit.debug();
        }
    
        private static void doSleep(long n) {
            try {
                Thread.sleep(n);
            }
            catch(InterruptedException e) {}
        }
    }
    

      反复运行JUnit测试,均未报错。统计结果如下:

      Total get: 10000000

      Not hit: 7

      Hits: 99%

      执行时间3.9秒。如果用synchronized取代ReadWriteLock,执行时间为204秒,可见性能差异巨大。

    总结:

      接口和实现的分离是必要的,否则难以实现Proxy模式。

      Facade模式和DAO模式都是必要的,否则,一旦数据访问分散在各个Servlet或JSP中,将难以控制缓存读写。

      下载完整的源代码

     作者简介
    廖雪峰 (dev2dev id: xuefengl ),长期从事J2EE/J2ME开发,对Open Source框架有深入研究,曾参与网易商城等大型J2EE应用的开发。目前廖雪峰创建了JavaEE开发网(http://www.javaeedev.com ),著有《Spring 2.0核心技术与最佳实践》一书。
  • 相关阅读:
    水洼,八连杀
    友链
    万能转换字符类型到int ,int到string,string到char or char *等等
    蓝桥杯模拟赛题
    2020 03 21
    2019 12 02 reading
    CentOS 7 定时计划任务设置
    xinted &telnet
    2019 12 02 section C one
    【暖*墟】#洛谷网课1.30# 树上问题
  • 原文地址:https://www.cnblogs.com/sunwei2012/p/1846425.html
Copyright © 2011-2022 走看看