简体   繁体   English

以同步,线程安全的方式加载共享资源

[英]Load shared resources in a synchronized, thread-safe way

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. 目前,我正在使用同步方法进行加载,并使用HashMap将加载的资源保留在内存中。

Question

Question 1: 问题1:

Is there a better way using standard Java means to achieve this? 有没有更好的方法使用标准Java手段来实现这一目标?

Question 2: 问题2:

The code below spawns 6 threads, 3 of them access resource 1, the other 3 access resource 2. The logging output is this: 下面的代码产生6个线程,其中3个访问资源1,其他3个访问资源2。日志记录输出是这样的:

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. 这样做的问题是,加载资源ID 1的线程被阻塞,直到加载资源ID 2的线程完成。 How is it possible to let the threads with resource id 1 continue even while resource id 2 is still loading? 即使资源ID 2仍在加载,如何允许资源ID为1的线程继续运行?

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);

    }

}

Just take a look at java.util.concurrent and java.util.concurrent.atomic packages. 只需看一下java.util.concurrentjava.util.concurrent.atomic包。

if I were you, I'd use ConcurrentHashMap : 如果我是你,我将使用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

Is there a better way using standard Java means to achieve this? 有没有更好的方法使用标准Java手段来实现这一目标?

Probably, but it depends on your requirements. 可能吧,但这取决于您的要求。 I tend to use computeIfAbsent on a ConcurrentMap to lazy load the values I need. 我倾向于在ConcurrentMap上使用computeIfAbsent来延迟加载所需的值。

eg 例如

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? 即使资源ID 2仍在加载,如何允许资源ID为1的线程继续运行?

You can do this if it is the same resource, assuming thread 1 doesn't need the resource that thread 2 is loading. 如果它是相同的资源,则可以执行此操作,假设线程1不需要线程2正在加载的资源。 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. 这很可能不是一个好主意。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM