在ConcurrentMap中存储线程是否安全?

时间:2017-09-12 11:01:14

标签: java multithreading concurrenthashmap

我正在构建一个后端服务,对我的服务进行REST调用会创建一个新线程。线程等待另一个REST调用,如果它没有收到任何东西,说5分钟线程将死亡。 为了跟踪所有线程,我有一个跟踪所有当前正在运行的线程的集合,以便当REST调用最终进入时,例如用户接受或拒绝某个操作,然后我可以使用userID识别该线程。如果它被拒绝,我们将从集合中删除该线程,如果它接受该线程可以继续执行下一个操作。我使用ConcurrentMap实现了这一点,以避免并发问题。

由于这是我第一次使用线程,我想确保我不会忽略可能出现的任何问题。请查看我的代码并告诉我是否可以做得更好或是否存在任何缺陷。

public class UserAction extends Thread {

    int     userID;
    boolean isAccepted  = false;
    boolean isDeclined  = false;
    long    timeNow     = System.currentTimeMillis();
    long    timeElapsed = timeNow + 50000;

    public UserAction(int userID) {
        this.userID = userID;
    }

    public void declineJob() {
        this.isDeclined = true;
    }

    public void acceptJob() {
        this.isAccepted = true;
    }

    public boolean waitForApproval(){   
        while (System.currentTimeMillis() < timeElapsed){
        System.out.println("waiting for approval");
        if (isAccepted) {
            return true;
        } else if (declined) {
            return false;
        }

    }
        return isAccepted;
    }

    @Override
    public void run() {
        if (!waitForApproval) {
            // mustve timed out or user declined so remove from list and return thread immediately
            tCollection.remove(userID);
            // end the thread here
            return;
        }
        // mustve been accepted so continue working
    }
}

public class Controller {

    public static ConcurrentHashMap<Integer, Thread> tCollection = new ConcurrentHashMap<>();

    public static void main(String[] args) {
        int barberID1 = 1;
        int barberID2 = 2;

        tCollection.put(barberID1, new UserAction(barberID1));
        tCollection.put(barberID2, new UserAction(barberID2));

        tCollection.get(barberID1).start();
        tCollection.get(barberID2).start();

        Thread.sleep(1000);
        // simulate REST call accepting/declining job after 1 second. Usually this would be in a spring mvc RESTcontroller in a different class.
        tCollection.get(barberID1).acceptJob();
        tCollection.get(barberID2).declineJob();

    }
}

2 个答案:

答案 0 :(得分:1)

您不需要(显式)线程。只是在第一次休息调用时创建的共享任务对象池。

当第二个休息呼叫到来时,你已经有一个线程要使用(正在处理其余呼叫的线程)。您只需要根据用户ID检索任务对象。您还需要摆脱过期的任务,例如DelayQueue

伪代码:

public void rest1(User u) {
    UserTask ut = new UserTask(u);
    pool.put(u.getId(), ut);
    delayPool.put(ut);  // Assuming UserTask implements Delayed with a 5 minute delay
}

public void rest2(User u, Action a) {
    UserTask ut = pool.get(u.getId());
    if(!a.isAccepted() || ut == null)
        pool.remove(u.getId());
    else
        process(ut);

    // Clean up the pool from any expired tasks, can also be done in the beginning
    // of the method, if you want to make sure that expired actions aren't performed
    while((UserTask u = delayPool.poll()) != null)
        pool.remove(u.getId());
}

答案 1 :(得分:1)

存在同步问题,您应该制作类isAccepted的标记isDeclinedAtomicBoolean。 一个关键概念是您需要采取措施确保将一个线程中的内存更改传递给需要该数据的其他线程。他们称之为内存防护,它们经常在同步调用之间隐式发生。

具有“中央记忆”的(简单)冯·诺依曼架构的想法&#39;对于大多数现代机器来说都是假的,你需要知道正确地在高速缓存/线程之间共享数据。

正如其他人所说,为每个任务创建一个线程是一个糟糕的模型。如果提交的任务太多,它会严重缩放并使您的应用程序容易受到影响。内存有一些限制,所以你一次只能有这么多待处理的任务,但线程的上限会低得多。 这会变得更糟,因为你在等待。旋转等待将线程放入等待条件的循环中。一个更好的模型会在ConditionVariable上等待,因此操作系统可以暂停不执行任何操作(除等待之外)的线程,直到通知他们等待的事情已经(或可能)准备就绪。 / p>

创建和销毁线程的时间和资源通常会有很大的开销。鉴于大多数平台可以同时仅执行相对较少数量的线程,从而产生大量昂贵的线程。让他们花费大部分时间换掉(暂停)无所事事的线程是非常低效的。

正确的模型启动固定数量的线程(或相对固定的数量)的池,并将任务放在线程“采取”的共享队列中。工作和处理。

该模型通常被称为&#34;线程池&#34;。 您应该看到的入门级实现是ThreadPoolExecutorhttps://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ThreadPoolExecutor.html