Java并发计数器没有正确清理

时间:2015-07-08 17:54:30

标签: java multithreading concurrency

这是一个java并发问题。需要完成10个工作,每个工作都有32个工作线程。工作线程会增加一个计数器。一旦计数器为32,表示此作业已完成,然后清理计数器图。从控制台输出,我希望10"完成"将输出,池大小为0,counterThread大小为0.

问题是:

  1. 大部分时间,"池大小:0和countThreadMap大小:3"将会 打印出来。即使那些线程都已消失,但3个工作都没有 完了。

  2. 有一段时间,我可以在第27行看到nullpointerexception。我使用过ConcurrentHashMap和AtomicLong,为什么还要有并发性 异常。

  3. 谢谢

    import java.util.concurrent.ConcurrentHashMap;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.ThreadPoolExecutor;
    import java.util.concurrent.atomic.AtomicLong;
    
    public class Test {
        final ConcurrentHashMap<Long, AtomicLong[]> countThreadMap = new ConcurrentHashMap<Long, AtomicLong[]>();
        final ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        final ThreadPoolExecutor tPoolExecutor = ((ThreadPoolExecutor) cachedThreadPool);
    
        public void doJob(final Long batchIterationTime) {
            for (int i = 0; i < 32; i++) {
                Thread workerThread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        if (countThreadMap.get(batchIterationTime) == null) {
                            AtomicLong[] atomicThreadCountArr = new AtomicLong[2];
                            atomicThreadCountArr[0] = new AtomicLong(1);
                            atomicThreadCountArr[1] = new AtomicLong(System.currentTimeMillis()); //start up time
                            countThreadMap.put(batchIterationTime, atomicThreadCountArr);
                        } else {
                            AtomicLong[] atomicThreadCountArr = countThreadMap.get(batchIterationTime);
                            atomicThreadCountArr[0].getAndAdd(1);
                            countThreadMap.put(batchIterationTime, atomicThreadCountArr);
                        }
    
                        if (countThreadMap.get(batchIterationTime)[0].get() == 32) {
                            System.out.println("done");
                            countThreadMap.remove(batchIterationTime);
                        }
                    }
                });
                tPoolExecutor.execute(workerThread);
            }
        }
    
        public void report(){
            while(tPoolExecutor.getActiveCount() != 0){
                //
            }
            System.out.println("pool size: "+ tPoolExecutor.getActiveCount() + " and countThreadMap size:"+countThreadMap.size());
        }
    
        public static void main(String[] args) throws Exception {
            Test test = new Test();
            for (int i = 0; i < 10; i++) {
                Long batchIterationTime = System.currentTimeMillis();
                test.doJob(batchIterationTime);
            }
    
            test.report();
            System.out.println("All Jobs are done");
    
        }
    }
    

2 个答案:

答案 0 :(得分:1)

让我们深入研究线程相关编程的所有错误,一个人可以做出:

Thread workerThread = new Thread(new Runnable() {
…
tPoolExecutor.execute(workerThread);

您创建了Thread但不启动它,而是将其提交给执行者。让Thread实现Runnable没有充分理由的Java API是一个历史错误。现在,每个开发人员都应该意识到,没有理由将Thread视为Runnable。如果您不想手动start线程,请不要创建Thread。只需创建Runnable并将其传递给executesubmit

我想强调后者,因为它返回一个Future,它可以免费提供你试图实现的内容:任务完成时的信息。使用invokeAll时会更容易,它会提交一堆Callable并在完成所有操作后返回。由于您没有告诉我们您的实际任务,因此不清楚您是否可以让您的任务简单地实现Callable(可能会返回null)而不是Runnable

如果你不能使用Callable或者不想在提交时立即等待,你必须记住返回的Future并在以后查询它们:

static final ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

public static List<Future<?>> doJob(final Long batchIterationTime) {
    final Random r=new Random();
    List<Future<?>> list=new ArrayList<>(32);
    for (int i = 0; i < 32; i++) {
        Runnable job=new Runnable() {
            public void run() {
                // pretend to do something
                LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(r.nextInt(10)));
            }
        };
        list.add(cachedThreadPool.submit(job));
    }
    return list;
}

public static void main(String[] args) throws Exception {
    Test test = new Test();
    Map<Long,List<Future<?>>> map=new HashMap<>();
    for (int i = 0; i < 10; i++) {
        Long batchIterationTime = System.currentTimeMillis();
        while(map.containsKey(batchIterationTime))
            batchIterationTime++;
        map.put(batchIterationTime,doJob(batchIterationTime));
    }
    // print some statistics, if you really need
    int overAllDone=0, overallPending=0;
    for(Map.Entry<Long,List<Future<?>>> e: map.entrySet()) {
        int done=0, pending=0;
        for(Future<?> f: e.getValue()) {
            if(f.isDone()) done++;
            else  pending++;
        }
        System.out.println(e.getKey()+"\t"+done+" done, "+pending+" pending");
        overAllDone+=done;
        overallPending+=pending;
    }
    System.out.println("Total\t"+overAllDone+" done, "+overallPending+" pending");
    // wait for the completion of all jobs
    for(List<Future<?>> l: map.values())
        for(Future<?> f: l)
            f.get();
    System.out.println("All Jobs are done");
}

但请注意,如果您不需要ExecutorService来执行后续任务,则等待所有作业完成会更容易:

cachedThreadPool.shutdown();
cachedThreadPool.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS);
System.out.println("All Jobs are done");

但无论手动跟踪工作状态多么不必要,让我们深入研究你的尝试,这样你就可以避免将来的错误:

if (countThreadMap.get(batchIterationTime) == null) {

ConcurrentMap是线程安全的,但这并不会将并发代码转换为顺序代码(这会使多线程无效)。上面的行可能同时由最多32个线程处理,都发现密钥不存在,因此可能会有多个线程将初始值放入映射中。

                    AtomicLong[] atomicThreadCountArr = new AtomicLong[2];
                    atomicThreadCountArr[0] = new AtomicLong(1);
                    atomicThreadCountArr[1] = new AtomicLong(System.currentTimeMillis());
                    countThreadMap.put(batchIterationTime, atomicThreadCountArr);

这就是为什么这被称为“检查然后行动”的反模式。如果有多个线程要处理该代码,那么他们都会put他们的新值,并确信这是正确的,因为他们在行动之前已经检查了初始条件但是除了一个线程以外的所有条件都有在行动时改变并且他们覆盖了之前put操作的值。

                } else {
                    AtomicLong[] atomicThreadCountArr = countThreadMap.get(batchIterationTime);
                    atomicThreadCountArr[0].getAndAdd(1);
                    countThreadMap.put(batchIterationTime, atomicThreadCountArr);

由于您正在修改已存储到地图中的AtomicInteger,因此put操作无用,它将放置之前检索到的数组。如果没有错误,如上所述可能存在多个初始值,则put操作无效。

                }

                if (countThreadMap.get(batchIterationTime)[0].get() == 32) {

同样,使用ConcurrentMap并不会将多线程代码转换为顺序代码。虽然很明显唯一的最后一个线程会将原子整数更新为32(当初始竞争条件没有实现时),但并不能保证所有其他线程都已经通过了这个if语句。因此,多达一个,所有线程仍然可以在此执行点并查看32的值。还是......

                    System.out.println("done");
                    countThreadMap.remove(batchIterationTime);

其中一个看到32值的线程可能会执行此remove操作。此时,可能仍有线程没有执行上述if语句,现在没有看到值32但是生成NullPointerException,因为数组应该包含AtomicInteger }不再在地图中了。这就是偶尔会发生的......

答案 1 :(得分:0)

创建10个作业后,您的main主题仍在运行 - 在report上调用test之前,它不会等待您的作业完成。您尝试使用while循环解决此问题,但在tPoolExecutor.getActiveCount()执行之前,0可能会以workerThread出现,然后countThreadMap.size()正在发生将线程添加到HashMap

之后

有很多方法可以解决这个问题 - 但我会让另一个回答者这样做,因为我现在必须离开。