使用ConcurrentMap实现高效可靠的原子操作

问题:服务器S1从远程获取多个文件到本地处理。这些文件的数据会被Processor转换成不同类型的数据模型存放至S1的数据库。每个Processor处理逻辑是相互独立的,但是同一个文件的数据可能会被多个Processor访问。为了提高数据模型的转换效率,需要将文件进行缓存,已经被缓存的文件不能被重复缓存。问题代码如下:

public class CacheManager 
{
    private Collection cachedFiles = new ArrayList<>();

    public void tryCache(String file)
    {
        if (!cachedFiles.contains(file))
        {
            doCache(file);
            cachedFiles.add(file);
        }
    }
}

在多线程环境下,tryCache方法的逻辑会存在由“线程交织”所带来的文件被重复缓存的问题。

以下几种解决方法点评一下:

1 一种错误的方法

cachedFiles字段使用诸如ConcurrentSkipListSet Collections.synchronizedSet,但是,该方法根本解决不了“线程交织”的问题。部分Java初学者容易犯这种错误。

2 一种不高效的方法

tryCache声明为synchronized方法,或者在if (!cachedFiles.contains(file))语句块外用synchronized(cachedFiles),来实现互斥。这方法能保证if (!cachedFiles.contains(file))块在任何时候只能被一个线程执行,的确能避免文件被重复缓存。但是性能不高,例如,如果Processor1要缓存文件AProcessor2要缓存文件B,两者并不冲突,但是两个Processor只能串行通过tryCache,却不能同时进行。

3 一种高效,但不可靠的方法

使用ConcurrentMapputIfAbsent实现高效的原子操作,但不可靠

经过改造的伪代码如下:

public class CacheManager 
{
    private Collection cachedFiles = new HashSet<>();


    private ConcurrentMap cacheTimestamp = new ConcurrentHashMap<>();


    public void tryCache(String file)
    {
        do
        {
            if (cacheTimestamp.putIfAbsent(file, System.currentTimeMillis()) == null)
            {
                if (!cachedFiles.contains(file))  
                {  
                    doCache(file);  
                    cachedFiles.add(file);  
                } 
                cacheTimestamp.remove(file);  
                break;
            }
            else
            {
                waitSomeTime();
            }
        }
        while(!Thread.interrupted());
    }
}

该方法能保证同一个file不会同时被多个Processor进行缓存,而且也能让处理不同fileProcessor并发进行缓存。

但是该方法却并不可靠:如果Processor1doCache(fileA)时发生异常导致cacheTimestamp.remove(fileA)不被执行,那么再也不会有其他Processor能通过cacheTimestamp.putIfAbsent(fileA, System.currentTimeMillis()) == null的校验,使得fileA永远不会再被缓存。cacheTimestamp也留下了一个垃圾记录。

一种高效,基本可靠的方法

public class CacheManager 
{
    private Collection cachedFiles = new HashSet<>();


    private ConcurrentMap cacheTimestamp = new ConcurrentHashMap<>();


    public void tryCache(String file)
    {
        do
        {
            if (cacheTimestamp.putIfAbsent(file, System.currentTimeMillis()) == null)
            {
                try  
                {  
                    if (!cachedFiles.contains(file))  
                    {  
                        doCache(file);  
                        cachedFiles.add(file);  
                    } 
                    break;
                }  
                finally  
                {  
                    cacheTimestamp.remove(file);  
                }
            }
            else
            {
                waitSomeTime();
            }
        }
        while(!Thread.interrupted());
    }
}

使用try ... finally ...来保证无论缓存成功与否,都能将cacheTimestamp中的记录清除。至此,这代码可以给个及格分了。什么,才及格?是的,请继续往下看

5 一种高效,靠超时机制来保证可靠性的方法

使用ConcurrentMap的putIfAbsent和replace方法,能实现上述问题的一种高效而可靠的解决方案。

public class CacheManager 
{
    private Collection cachedFiles = new HashSet<>();

    private ConcurrentMap cacheTimestamp = new ConcurrentHashMap<>();
    
    private final long TIMEOUT = 600000;

    public void tryCache(String file)
    {
        do
        {
            Long timestamp = cacheTimestamp.putIfAbsent(file, System.currentTimeMillis() + TIMEOUT)
            if (timestamp == null)
            {
                timestamp = cacheTimestamp.get(file);

                try  
                {  
                    if (!cachedFiles.contains(file))  
                    {  
                        doCache(file);  
                        cachedFiles.add(file);  
                    } 
                    break;
                }  
                finally  
                {  
                    cacheTimestamp.remove(file, timestamp);  
                }
            }
            else if (System.currentTimeMillis() > timestamp) // 缓存file超时
            {
                if(cacheTimestamp.replace(file, timestamp, System.currentTimeMillis() + TIMEOUT))
                {
                    try  
                    {  
                        if (!cachedFiles.contains(file))  
                        {  
                            doCache(file);  
                            cachedFiles.add(file);  
                        } 
                        break;
                    }  
                    finally  
                    {  
                        cacheTimestamp.remove(file, timestamp);  
                    }
                }
            }
            else
            {
                wait(timestamp - System.currentTimeMillis());
            }
        }
        while(!Thread.interrupted());
    }
}

一种高效,靠等待机制来保证可靠性的方法

使用ConcurrentMap的putIfAbsent方法和CountDownLatch对象,能实现上述问题的另一种高效而可靠的解决方案。

public class CacheManager 
{
    private Collection cachedFiles = new HashSet<>();

    private ConcurrentMap cacheTimestamp = new ConcurrentHashMap<>();

    public void tryCache(String file)
    {
        CountDownLatch signal = cacheTimestamp.putIfAbsent(file, new CountDownLatch(1))
        if (signal == null)
        {
            signal = cacheTimestamp.get(file);
            try  
            {  
                if (!cachedFiles.contains(file))  
                {  
                    doCache(file);  
                    cachedFiles.add(file);  
                } 
                break;
            }  
            finally  
            {  
                signal.countDown(); 
                cacheTimestamp.remove(file);
            }
        }
        else
        {
            signal.await();
        }
    }
}


你可能感兴趣的:(Note)