可能你碰到过需要在服务器中缓存数据的情况。缓存数据用来对查询的请求做出响应。在其中可能会使用一个线程来监听对缓存的更新事件。该线程作为这些事件的响应最终对缓存数据进行修改。
这就产生了一个问题,也就是不太可能让服务于查询请求的线程和更新缓存的线程同时对缓存进行操作。当然,可以使用诸如Java同步之类的排他锁来处理这个问题。但这种方法从性能的角度来考虑几乎是最差的。本博文将使用效率更高的读写锁技术来解决此问题。
读写锁
刚才的解决方案中的致命问题在于同步块只能让一个线程访问被保护的资源。当我们对缓存执行查询,并不对其进行修改时,这种方案就显得不优化了。所以如果我们只有只读线程访问缓存的话,我们就根本不需要同步,所有的线程都将可以对数据结构进行同时安全的访问,因为我们并不会修改数据结构。但是,如果有个线程想要修改数据又该如何呢?如果我们让读写线程同时对缓存进行操作,缓存就有崩溃的危险。我们将使用读写锁来解决问题。它从根本上说就是允许多个读线程访问被保护资源。这些读线程在读取的时候必须获得一个读锁并在读取完毕后对读锁进行释放。写线程,与线程不一样,需要在写入的时候获取一个写锁,并在写完后释放。
我们可对读写锁的行为描述如下:我们允许读线程无限制地访问资源。然而,如果写线程需要访问缓存的话,我们就:
- 让后续的读线程进入读取队列,暂时阻止读线程。
- 等待正在操作的读线程释放读锁。这时缓存中可能还有多个读线程正在读取数据,我们必须先等待他们释放锁。
- 授予写线程访问权限。当缓存中的所有读线程完成操作且后续的读线程已被存入队列中等待后,我们就可以让写线程写入了。
- 写线程释放写锁。当写线程释放写锁之时,我们就可以将读取队列中的读线程从对列中取出,授予他们访问缓存的权限。
所以,我们现在基本上实现了并行读取,串行写入。
如何获得读写锁呢?
创建锁:
我们需要创建读写锁。我们采用单实例模式完成。
import cn.com.betteryou.util.concurrent.*;
public class MyLock {
static ReadWriteLock myLock;
public static ReadWriteLock getLock()
{
if(myLock == null)
{
synchronized (MyLock.class)
{
if(myLock == null)
{
myLock = new WriterPreferenceReadWriteLock();
}
}
}
return myLock;
}
}
当读线程或写线程需要获取锁时,我们即调用MyLock.getLock()。
读线程示例:
public void serviceReader()
throws InterruptedException
{
ReadWriteLock rwLock = MyLock.getLock();
Sync s = rwLock.readLock();
s.acquire();
try
{
// 访问缓存
}
finally
{
s.release();
}
}
写线程示例:
public void serviceWriter()
throws InterruptedException
{
ReadWriteLock rwLock = MyLock.getLock();
Sync s = rwLock.writeLock();
s.acquire();
try
{
// 写缓存
}
finally
{
s.release();
}
}
使用规则:
保证读线程和写线程不嵌套获取锁。
如何在现实场景中使用
要在现实环境中使用缓存,除了读写锁之外,还需要考虑如何侦测数据库更新以便对缓存进行操作。通常的做法是在表上建立触发器。触发器调用存储过程来进行整体的处理。对于主流的数据库供应商来说,比如Oracle和Sybase,它们都内置了JMS的消息机制,可以编写相应的逻辑让数据库和应用服务器的缓存协同工作起来。