设计:一次性使用的物品?
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
}
另外一个想法:线程创建是昂贵的。
创建线程池(JavaAPI有支持:http://docs.oracle.com/javase/tutorial/essential/concurrency/pools.html)
导出Runnable
class来实现你的"task"。
将您的 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);
}
}
上下文
我有一个叫 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
}
另外一个想法:线程创建是昂贵的。
创建线程池(JavaAPI有支持:http://docs.oracle.com/javase/tutorial/essential/concurrency/pools.html)
导出
Runnable
class来实现你的"task"。将您的 Runnable 提交到线程池执行器服务。
pictureID
应该是您的 Runnable 派生的实例变量 class。您的 Runnable 派生的 class 可以(但不必)实现为您的 ImageLoader
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);
}
}