上下文

我有一个叫做ImageLoader的类。

当我调用ImageLoader的getPicture( pictureID )时,如果未缓存图片,则将pictureID存储在一个实例变量中,创建一个新线程,该线程最终在同一callback()类中调用ImageLoader函数。然后callback()使用此pictureID作为缓存图片的键。

问题

你看到泡菜了吗?
如果我连续两次调用getPicture( somePictureID ),而第一次调用的callback()函数尚未发生,我将覆盖pictureID函数将使用的先前的callback()。 (如果您仍然看不到泡菜,请参见下面的代码。)

现在我知道您在想,为什么不只在变量上添加一点同步以使其成为线程安全的呢?

因为谁知道查询图片的线程将花费多长时间,这确实会减慢加载多张图片的整个过程。

我如何解决它的想法

因此,我的绝妙主意是创建一个内部类,该内部类将服务器作为从属服务器,并且只能使用一次。我只在该从属类中执行过一些功能,并且从不重用该对象。

问题

这是解决此类问题的合适方法吗?我觉得这可能会陷入我不知道的某种模式。如果我对此一无所知,您能建议另一种解决方案吗?

简化代码

//the instance variable with a race condition
private int pictureID

//loads the image associated with the pictureID
public void getPicture( int pictureID )
{
     Bitmap bitmap = cache.get( pictureID );
     if ( bitmap != null )
     {
         //load image
         return;
     }

     int post_params[] = { pictureID, /* more parameters */ };

     this.pictureID = pictureID; //PROBLEM: race condition!
     new HttpRequest( this ).execute( post_params ); //starts new thread and queries server
}

 //gets called when the query finishes, json contains my image
 public void callback( JSONObject json )
 {
    Bitmap bitmap = getBitmap( json ); //made up method to simplify code
    cache.put( this.pictureID, bitmap ); //PROBLEM: race condition!

    //load image
 }

最佳答案

对我来说,这看起来像是Memoizer模式的经典案例,最简单的使用方法可能是Guava的CacheBuilder,例如:

private LoadingCache<Integer, Image> cache = CacheBuilder.newBuilder().
        build(new CacheLoader<Integer, Image>() {
            @Override
            public Image load(Integer key) throws Exception {
                return httpGetImageExpensively(key);
            }
        });

public Image getPicture(int pictureId) {
   return cache.getUnchecked(pictureId); // blocks until image is in cache
}

现在,HTTP请求将在第一个调用给定ID的getPicture()的线程中发生;具有该ID的后续调用者将阻塞,直到第一个线程将图像放入缓存中为止,但您可以同时请求多个ID。

要考虑的事情:
  • 加载速度有多慢?您需要超时机制吗?
  • 如果加载失败怎么办? cache.get()方法抛出一个(已检查的)ExecutionException,这就是为什么我改用getUnchecked()的原因,但是它只是抛出了一个(未经检查的)UncheckedExecutionException。如果可能的话,我会在httpGetImageExpensively()中进行大多数错误处理,并仔细考虑那里的可能情况(错误的ID,DNS故障等),但是getPicture()的调用者仍需要以某种方式处理该错误。
  • 如果加载给定的ID一次失败,那么每次都会失败吗?如果是这样,您可能不想浪费时间每次都重新进行HTTP-GET,因此您需要为此添加一种机制。但是请注意,对于不同类型的故障,是否值得重试的问题可能会有不同的答案。
  • 此实现阻止每个调用线程,直到准备好映像为止。那对这个应用程序可行吗?如果不是这样,您可能需要用一些需要图像处理程序回调对象或lambda的东西替换getPicture(),然后在内部将对httpGetImageExpensively()的实际调用放在ExecutorService上,以便getPicture()可以立即返回。 (取决于应用程序,可能还有其他问题,例如在Swing应用程序中,可能希望在事件分配线程上调用回调。)
  • 08-16 05:09