设计:一次性使用的物品?

Design: Object with single use?

上下文

我有一个叫 ImageLoader 的 class。

当我调用ImageLoader的getPicture( pictureID )时,如果图片没有被缓存,我将pictureID存储在一个实例变量中,创建一个新线程最终调用我的callback()函数同样 ImageLoader class。 callback()然后用这个pictureID作为key来缓存图片

问题

你看到泡菜了吗? 如果我连续两次调用 getPicture( somePictureID ),并且第一次调用的 callback() 函数还没有发生,我将覆盖之前的 pictureID,而 callback() 函数将利用。 (如果您仍然没有看到 pickle,请参阅下面的代码。)

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

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

我是怎么想解决的

所以我的好主意是创建一个内部 class 作为奴隶服务器并且只能使用一次。我在这个 slave class 中执行了一些函数一次,我从不重用这个对象。

问题

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

简化代码

//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
 }

另外一个想法:线程创建是昂贵的。

  1. 创建线程池(JavaAPI有支持:http://docs.oracle.com/javase/tutorial/essential/concurrency/pools.html

  2. 导出Runnableclass来实现你的"task"。

  3. 将您的 Runnable 提交到线程池执行器服务。

pictureID 应该是您的 Runnable 派生的实例变量 class。您的 Runnable 派生的 class 可以(但不必)实现为您的 ImageLoader class.

的内部 class

编辑:示例。

some_perhaps_inner_class MyRunable implements Runnable {
    private int someImportantId;

    MyRunnable(int someImportantId) {
        this.someImportantId = someImportantId;
    }

    @Override
    public void run() {
        doTheTimeConsumingStuffHere();
    }
}

ExecutorService myThreadPool = Executors.newFixedThreadPool(3);
myThreadPool.submit(new MyRunnable(someImportantId));

详情取决于您的特殊需求。请查看 Java API 以进一步阅读。

对我来说,这看起来像是 Memoizer pattern, and the easiest way to use it is probably Guava's 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() 可以 return 立即。 (根据应用程序,可能存在其他问题,例如,在 Swing 应用程序中,可能需要在事件分派线程上调用回调。)

HttpRequest如何知道我们需要哪张图片?我们不会发送有关它的任何信息(pictureId 是私有字段)。如果它不相关(pictureId - 请求的图片)就使用优先级队列。

//queue contains pictures id queue to load
private Queue<Integer> pictureIDQueue = new LinkedList<Integer>();

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

     synchronized(pictureIDQueue)
     {
         this.pictureIDQueue.add(pictureID);
     }
     new HttpRequest( this ).execute(); //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

    synchronized(pictureIDQueue)
    {
        cache.put( this.pictureIDQueue.poll(), bitmap );
    }
    //load image
 }

您也可以这样尝试:

private Map<Integer, Bitmap> cache = new HashMap<Integer,Bitmap>();
private Queue<Thread> threads = new LinkedList<Thread>();
//the instance variable with a race condition
private int pictureID;

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

     int post_params[] = { pictureID, /* more parameters */ };
     final ImageLoader instance = this;

     Thread currentThread = new Thread()
     {
         @Override
         public void run()
         {
             pictureID = pmPictureID;
             new HttpRequest(instance).execute( post_params ); //starts new thread and queries server
         }
     };

     if(threads.size() > 0)
     {
         threads.add(currentThread);
     }
     else
     {
         currentThread.start();
     }
}

 //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!

    if(threads.peek() != null)
    {
        threads.poll().start();
    }
    //load image
 }

这 returns 位图 Future 不是处理回调,您可以阻止它直到准备就绪。如果您依赖多个位图并且不想单独阻止所有位图,我会对其进行改造以与 Guava Futures 一起使用。

public class AsyncMemoizingImageLoader {
    private final ConcurrentMap<Integer, Future<Bitmap>> bitmaps = new ConcurrentHashMap<>();
    private final ExecutorService executor = Executors.newFixedThreadPool(8);

    // TODO: shutdown executor when complete

    public Future<Bitmap> getPicture(final int pictureID) {
        if (!bitmaps.contains(pictureID)) {
            RunnableFuture<Bitmap> future = new FutureTask<Bitmap>(new Callable<Bitmap>() {
                @Override public Bitmap call() throws Exception {
                    // HTTP stuff goes here.  pictureID is accessible in this scope
                }
            });

            // This bit is key; only submit the future for completion if the
            // picture id isn't already being processed
            if (bitmaps.putIfAbsent(pictureID, future) == null) {
                executor.submit(future);
            }
        }

        return bitmaps.get(pictureID);
    }
}