外部共享资源(智能卡)的Java并发模式

时间:2015-12-07 16:46:58

标签: java multithreading concurrency smartcard threadpoolexecutor

我有一个Web服务器服务,客户端请求智能卡计算并获得结果。 在服务器正常运行期间,可用的智能卡号可以减少或增加,例如,我可以从阅读器中添加或删除智能卡(或许多其他事件......例如异常等)。

enter image description here

智能卡计算可能需要一段时间,因此如果有对Web服务器的并发请求,我必须优化这些作业以使用所有可用的智能卡。

我想过使用智能卡线程池。至少对我而言,不寻常的是,池应该改变其大小,而不是取决于客户端的请求,而只取决于智能卡的可用性。

enter image description here

我研究了许多例子:

  • BlockingQueue :存储请求并停止线程等待某事做的好看。
  • FutureTask :我可以使用这个类来让客户端等待它的答案,但是哪种类型的执行者应该完成任务?
  • ThreadPoolExecutor :似乎我需要什么,但有了这个我无法改变池大小,而且每个线程都应该链接到一个智能卡插槽。如果我可以更改池大小(在插入智能卡时添加线程并在删除智能卡时删除线程)以及是否可以为每个线程分配特定的智能卡,这可以是一种解决方案。

这是智能卡控件,每个智能卡有一个SmartcardWrapper,每个智能卡都有自己的插槽号。

public class SmartcardWrapper{

    private int slot;

    public SmartcardWrapper(int slot) {
        this.slot=slot;
    }   

    public byte[] compute(byte[] input) {
        byte[] out=new byte[];
        SmartcardApi.computerInput(slot,input,out); //Native method
        return out;
    }
}

我尝试使用每个智能卡一个线程创建一个线程池:

private class SmartcardThread extends Thread{

    protected SmartcardWrapper sw;

    public SmartcardThread(SmartcardWrapper sw){
        this.sw=sw;
    }

    @Override
    public void run() {
        while(true){
            byte[] input=queue.take();
            byte output=sw.compute(input);
            // I have to return back the output to the client
        }           
    }
}

每个人都在等待同一输入队列中的某些内容:

BlockingQueue<byte[]> queue=new BlockingQueue<byte[]>();

但是如何将智能卡线程的输出返回给webserver-client呢?这让我觉得BlockingQueue不是我的解决方案。

如何解决这个问题?我应该遵循哪种并发模式? 为每个智能卡分配一个线程是否正确,或者我是否可以简单地使用信号量?

5 个答案:

答案 0 :(得分:6)

你的假设:

  

ThreadPoolExecutor:似乎我需要什么,但有了这个我不能改变池大小,而且每个线程都应该链接到一个智能卡插槽。

不对。

<强> You can set thread pool size dynamically.

查看以下ThreadPoolExecutor API

public void setMaximumPoolSize(int maximumPoolSize)
  

设置允许的最大线程数。这将覆盖构造函数中设置的任何值。如果新值小于当前值,则过多的现有线程将在下一次空闲时终止。

public void setCorePoolSize(int corePoolSize)
  

设置核心线程数。这将覆盖构造函数中设置的任何值。如果新值小于当前值,则当下一个空闲时,多余的现有线程将被终止。如果需要更大,则可以启动新线程来执行任何排队任务。

Core and maximum pool sizes:

ThreadPoolExecutor会根据corePoolSizemaximumPoolSize设置的范围自动调整池大小。

当在方法execute(java.lang.Runnable)中提交新任务且运行的线程少于corePoolSize时,即使其他工作线程处于空闲状态,也会创建一个新线程来处理该请求。

如果有超过corePoolSize但少于maximumPoolSize个线程正在运行,则只有在队列已满时才会创建新线程。

通过将maximumPoolSize设置为基本无限制的值(例如Integer.MAX_VALUE),您可以允许池容纳任意数量的并发任务。但我不建议拥有那么多线程。请谨慎设置此值。

最典型的核心和最大池大小仅在构建时设置,但也可以使用setCorePoolSize(int)和setMaximumPoolSize(int)动态更改。

修改

为了更好地利用线程池,如果您知道最大卡数为6,则可以使用

 ExecutorService executor = Executors.newFixedThreadPool(6);

OR

答案 1 :(得分:5)

您是否考虑过使用Apache Commons Pool

您需要维护一个SmartcardWrapper对象池,其中每个SmartcardWrapper将代表一个物理SmartCard。无论何时需要进行新计算,都要从池中借用对象,进行计算并返回池中的对象,以便下一个线程可以重用它。

池本身是线程安全的,并且在没有可用对象时阻塞。您需要做的就是实现一个api来向池中添加/删除SmartcardWrapper对象。

答案 2 :(得分:3)

我可能会根据以下假设找到一个合理的简单解决方案:

  • 一个单独的流程管理可用或被删除的智能卡的(系统事件)通知。
  • 客户不关心它可以使用哪个智能卡,只要它可以使用一个没有干扰的智能卡。

这两个假设实际上使创建池(共享资源)解决方案变得更加容易,因为池本身通常负责在适当时创建和删除资源。如果没有此功能,池化解决方案将变得更加简单。我假设从池中获取智能卡的客户端可以在其自己的执行线程中执行所需的智能卡功能(类似于如何从数据库连接池使用数据库连接来查询数据库中的数据)。 / p>

我只对下面显示的两个类进行了一些最小的测试,我担心大部分工作是写入(单元)测试,证明池与并发客户端请求相结合以及添加和删除智能卡资源。如果您不想这样做,那么answer from user769771可能是更好的解决方案。但如果你这样做,试一试,看它是否合适。我们的想法是,所有客户端只创建和使用一个资源池实例,并由管理智能卡可用性的单独进程进行更新。

import java.util.*;
import java.util.concurrent.*;

/**
 * A resource pool that expects shared resources 
 * to be added and removed from the pool by an external process
 * (i.e. not done by the pool itself, see {@link #add(Object)} and {@link #remove(Object)}.
 * <br>A {@link ResourcePoolValidator} can optionally be used. 
 * @param <T> resource type handed out by the pool.
 */
public class ResourcePool<T> {

    private final Set<T> registered = Collections.newSetFromMap(new ConcurrentHashMap<T, Boolean>()); 
    /* Use a linked list as FIFO queue for resources to lease. */
    private final List<T> available = Collections.synchronizedList(new LinkedList<T>()); 
    private final Semaphore availableLock = new Semaphore(0, true); 

    private final ResourcePoolValidator<T> validator;

    public ResourcePool() {
        this(null);
    }

    public ResourcePool(ResourcePoolValidator<T> validator) {
        super();
        this.validator = validator;
    }

    /**
     * Add a resource to the pool.
     * @return true if resource is not already in the pool.
     */
    public synchronized boolean add(T resource) {

        boolean added = false;
        if (!registered.contains(resource)) {
            registered.add(resource);
            available.add(resource);
            availableLock.release();
            added = true;
        }
        return added;
    }

    /**
     * Removes a resource from the pool.
     * The resource might be in use (see {@link #isLeased(Object)})
     * in which case {@link ResourcePoolValidator#abandoned(Object)} will be called 
     * when the resource is no longer used (i.e. released). 
     * @return true if resource was part of the pool and removed from the pool.
     */
    public synchronized boolean remove(T resource) {

        // method is synchronized to prevent multiple threads calling add and remove at the same time 
        // which could in turn bring the pool in an invalid state.
        return registered.remove(resource);
    }

    /**
     * If the given resource is (or was, see also {@link #remove(Object)} part of the pool,
     * a returned value true indicates the resource is in use / checked out.
     * <br>This is a relative expensive method, do not call it frequently.
     */
    public boolean isLeased(T resource) {
        return !available.contains(resource);
    }

    /**
     * Try to get a shared resource for usage. 
     * If a resource is acquired, it must be {@link #release(Object)}d in a finally-block.
     * @return A resource that can be exclusively used by the caller.
     * @throws InterruptedException When acquiring a resource is interrupted.
     * @throws TimeoutException When a resource is not available within the given timeout period.
     */
    public T tryAcquire(long timeout, TimeUnit tunit) throws InterruptedException, TimeoutException {

        T resource = null;
        long timeRemaining = tunit.toMillis(timeout);
        final long tend = System.currentTimeMillis() + timeRemaining;
        do {
            if (availableLock.tryAcquire(timeRemaining, TimeUnit.MILLISECONDS)) {
                resource = available.remove(0);
                if (registered.contains(resource)) {
                    boolean valid = false;
                    try {
                        valid = (validator == null ? true : validator.isValid(resource));
                    } catch (Exception e) {
                        // TODO: log exception
                        e.printStackTrace();
                    }
                    if (valid) {
                        break; // return the "checked out" resource
                    } else {
                        // remove invalid resource from pool
                        registered.remove(resource);
                        if (validator != null) {
                            validator.abandoned(resource);
                        }
                    }
                }
                // resource was removed from pool, try acquire again
                // note that this implicitly lowers the maximum available resources
                // (an acquired permit from availableLock goes unused).
                // TODO: retry puts us at the back of availableLock queue but should put us at the front of the queue
                resource = null;
            }
            timeRemaining = tend - System.currentTimeMillis();
        } while (timeRemaining > 0L);
        if (resource == null) {
            throw new TimeoutException("Unable to acquire a resource within " + tunit.toMillis(timeout) + " ms.");
        }
        return resource;
    }

    /**
     * This method must be called by the caller / client whenever {@link #tryAcquire(long, TimeUnit)}
     * has returned a resource. If the caller has determined the resource is no longer valid,
     * the caller should call {@link #remove(Object)} before calling this method.
     * @param resource no longer used.
     */
    public void release(T resource) {

        if (resource == null) {
            return;
        }
        if (registered.contains(resource)) {
            available.add(resource);
            availableLock.release();
        } else {
            if (validator != null) {
                validator.abandoned(resource);
            }
        }
    }

    /** An array (copy) of all resources registered in the pool. */
    @SuppressWarnings("unchecked")
    public T[] getRegisteredResources() {
        return (T[]) registered.toArray(new Object[registered.size()]);
    }

}

一个单独的类,其功能与管理smarcard可用性的单独进程相关。

import java.util.concurrent.TimeUnit;

/**
 * Used by a {@link ResourcePool} to validate a resource before handing it out for lease
 * (see {@link #isValid(Object)} and signal a resource is no longer used (see {@link #abandoned(Object)}). 
 */
public class ResourcePoolValidator<T> {

    /**
     * Overload this method (this method does nothing by default) 
     * to validate a resource before handing it out for lease.
     * If this method returns false or throws an exception (which it preferably should not do), 
     * the resource is removed from the pool.
     * @return true if the resource is valid for leasing
     */
    public boolean isValid(T resource) {
        return true;
    }

    /**
     * Called by the {@link ResourcePool#release(Object)} method when a resource is released by a caller 
     * but the resource was previously removed from the pool and in use.
     * <br>Called by {@link ResourcePool#tryAcquire(long, TimeUnit)} if a resource if not valid 
     * (see {@link #isValid(Object)}.
     * <br>Overload this method (this method does nothing by default) to create a notification of an unused resource,
     * do NOT do any long period of processing as this method is called from a caller (client) thread.
     */
    public void abandoned(T resource) {
        // NO-OP
    }

}

答案 3 :(得分:2)

通过查看要求,最佳架构可以将智能卡的计算与Web服务分离。

依靠Web服务等待处理器密集型任务将导致超时。

最佳解决方案是使用定期作业预先计算智能卡,并将这些插槽,计算对存储在像Redis这样的高速缓存服务器中。

enter image description here

智能卡同步器作业是一个单独的J2SE独立应用程序,它定期检查哪些智能卡可用且处于活动状态(无错误),并使用插槽和计算作为键/值对更新Redis缓存。如果智能卡不可用,它将从缓存中删除。

Web服务只检查Redis缓存中的特定槽键,如果找到值则返回它,否则返回找不到该槽(不可用或错误)

此设计在智能卡端和客户端请求端均可扩展。

答案 4 :(得分:2)

回答有关如何将结果返回给调用者的问题:

  

每个人都在等待同一输入队列中的某些内容:

     

BlockingQueue queue = new BlockingQueue();

     

但是如何将智能卡线程的输出返回给   Web服务器的客户端?这让我觉得BlockingQueue不是我的   溶液

您的提交队列想法大部分没什么问题,但是每个线程还需要一个队列来将结果返回给作业提交者......

将提交队列更改为:

BlockingQueue<JobSubmitRec> queue=new BlockingQueue<JobSubmitRec>();

和JobSubmitRec将使用byte []和一次性队列来返回结果:

class JobSubmitRec
{
  byte[] data;
  BlockingQueue<JobSubmitResult> result=new LinkedBlockingQueue<JobSubmitResult>();
}

你的工作者Thread runnable看起来像是:

public void run() {
 while(true){
  JobSubmitRec submitrec = queue.take();
  byte[] input = submitrec.data;
  byte output = sw.compute(input);
  submitrec.result.put( new JobSubmitResult(output) );
 }           
}

并且提交作业的客户端将如下所示:

JobSubmitRec jsr = new JobSubmitRec( data );
queue.put( jsr );
JobSubmitResult result = jsr.result.take();
// use result here