Reputation: 18415
Problem
I'd like to load shared resources only once and keep them in memory. Currently I'm using a synchronized method for the loading and a HashMap for keeping the loaded resources in memory.
Question
Question 1:
Is there a better way using standard Java means to achieve this?
Question 2:
The code below spawns 6 threads, 3 of them access resource 1, the other 3 access resource 2. The logging output is this:
Get resource id: 1
Load resource id: 1
Counter: 1
Get resource id: 2
Thread Thread[Thread-0,5,main], loaded resource id: 1
Load resource id: 2
Counter: 2
Thread Thread[Thread-5,5,main], loaded resource id: 2
Get resource id: 2
Thread Thread[Thread-4,5,main], loaded resource id: 2
Get resource id: 2
Thread Thread[Thread-3,5,main], loaded resource id: 2
Get resource id: 1
Get resource id: 1
Thread Thread[Thread-2,5,main], loaded resource id: 1
Thread Thread[Thread-1,5,main], loaded resource id: 1
The problem with that is that the threads which load resource id 1 are blocked until the thread which loads resource id 2 is finished. How is it possible to let the threads with resource id 1 continue even while resource id 2 is still loading?
Code
Here's the example code:
import java.util.HashMap;
import java.util.Map;
public class SharedResourceLoader {
/**
* Resource loading counter, shows how often the loading is invoked.
*/
public static int loadCounter = 0;
/**
* Map of type <Resource Id, Resource> for keeping loaded resources in memory
*/
public static Map<Integer,Resource> resourceMap = new HashMap<>();
/**
* Get a resource by Id
* @param id
* @return
*/
public static Resource getResource( int resourceId) {
Resource resource = resourceMap.get( resourceId);
if( resource == null) {
resource = loadResource( resourceId);
}
return resource;
}
/**
* Get a resource by Id synchronized. If it isn't found, load it.
* @param resourceId
* @return
*/
public static synchronized Resource loadResource( int resourceId) {
System.out.println("Get resource id: " + resourceId);
Resource resource = resourceMap.get( resourceId);
if( resource == null) {
System.out.println("Load resource id: " + resourceId);
// load resource
resource = new Resource( resourceId);
resource.load();
// keep resource in memory
resourceMap.put( resourceId, resource);
// just a counter to see how often this method is accessed
loadCounter++;
System.out.println("Counter: " + loadCounter);
}
return resource;
}
/**
* Start a thread that accesses the resource with the given id
* @param resourceId
*/
public static void spawnThread( int resourceId) {
Thread thread = new Thread( new Runnable() {
@Override
public void run() {
Resource resource = getResource(resourceId);
System.out.println( "Thread " + Thread.currentThread() + ", loaded resource id: " + resource.id);
}
});
thread.start();
}
public static class Resource {
int id;
public Resource( int id) {
this.id = id;
}
public void load() {
// dummy sleep, e. g. resource loading happens here
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
}
}
}
public static void main(String[] args) {
spawnThread( 1);
spawnThread( 1);
spawnThread( 1);
spawnThread( 2);
spawnThread( 2);
spawnThread( 2);
}
}
Upvotes: 0
Views: 518
Reputation: 1359
Just take a look at java.util.concurrent
and java.util.concurrent.atomic
packages.
if I were you, I'd use ConcurrentHashMap
:
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
public class SharedResourceLoader {
/**
* Resource loading counter, shows how often the loading is invoked.
*/
private static final AtomicInteger loadCounter = new AtomicInteger();
/**
* Map of type <Resource Id, Resource> for keeping loaded resources in memory
*/
public static ConcurrentMap<Integer, Resource> resourceMap = new ConcurrentHashMap<>();
/**
* Get a resource by Id
*
* @param resourceId
* @return
*/
public static Resource getResource(int resourceId) {
Resource resource = resourceMap.get(resourceId);
if (resource == null) {
resource = loadResource(resourceId);
}
return resource;
}
/**
* Get a resource by Id synchronized. If it isn't found, load it.
*
* @param resourceId
* @return
*/
public static Resource loadResource(int resourceId) {
System.out.println("Get resource id: " + resourceId);
Resource resource = resourceMap.get(resourceId);
if (resource == null) {
System.out.println("Load resource id: " + resourceId);
// load resource
final Resource r = resourceMap.putIfAbsent(resourceId, resource = new Resource(resourceId));
// important!
if (r != null) {
resource = r;
}
if (resource.load()) {
// just a counter to see how often this method is accessed
loadCounter.getAndIncrement();
}
System.out.println("Counter: " + loadCounter);
}
return resource;
}
public static int loadCounter() {
return loadCounter.get();
}
/**
* Start a thread that accesses the resource with the given id
*
* @param resourceId
*/
public static void spawnThread(int resourceId) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
Resource resource = getResource(resourceId);
System.out.println("Thread " + Thread.currentThread() + ", loaded resource id: " + resource.id);
}
});
thread.start();
}
public static class Resource {
final int id;
final AtomicBoolean loaded = new AtomicBoolean(false);
public Resource(int id) {
this.id = id;
}
public boolean load() {
if (loaded.compareAndSet(false, true)) {
// dummy sleep, e. g. resource loading happens here
try {
Thread.sleep(5000);
} catch (InterruptedException ignored) {
}
return true;
}
return false;
}
}
public static void main(String[] args) {
spawnThread(1);
spawnThread(1);
spawnThread(1);
spawnThread(2);
spawnThread(2);
spawnThread(2);
}
}
Output :
Get resource id: 1
Get resource id: 2
Get resource id: 2
Get resource id: 2
Get resource id: 1
Get resource id: 1
Load resource id: 1
Load resource id: 2
Load resource id: 2
Load resource id: 2
Load resource id: 1
Load resource id: 1
Counter: 0
Counter: 0
Counter: 0
Counter: 0
Thread Thread[Thread-5,5,main], loaded resource id: 2
Thread Thread[Thread-4,5,main], loaded resource id: 2
Thread Thread[Thread-1,5,main], loaded resource id: 1
Thread Thread[Thread-2,5,main], loaded resource id: 1
Counter: 1
Thread Thread[Thread-3,5,main], loaded resource id: 2
Counter: 2
Thread Thread[Thread-0,5,main], loaded resource id: 1
Upvotes: 2
Reputation: 533720
Is there a better way using standard Java means to achieve this?
Probably, but it depends on your requirements. I tend to use computeIfAbsent
on a ConcurrentMap
to lazy load the values I need.
e.g.
static final ConcurrentMap<Integer, Resource> map = new ConcurrentHashMap<>();
static Resource loadResource(int resourceId) {
return map.computeIfAbsent(resourceId, r -> {
Resource resource = new Resource(r);
resource.load();
return resource;
}
}
This will allow concurrent access to different keys, though if a key is being load it will block any other thread which attempts to use it until loaded.
How is it possible to let the threads with resource id 1 continue even while resource id 2 is still loading?
You can do this if it is the same resource, assuming thread 1 doesn't need the resource that thread 2 is loading. If you referring to different resources, see above. You can check whether the data is being loaded and if it is don't attempt to look in the map. Most likely this is not a good idea.
Upvotes: 1