我正在创建一个java类idgenerator,它在每次请求时都分配一个唯一的整数id。它使用treeset来存储空闲id的范围,每次请求id时,它都会在集合中查找范围,分配范围中的第一个id,删除范围,并添加一个较小的新范围。整个分配过程在集合上同步,以确保不同的线程不会冲突。
当我对这个类进行单元测试时,这个方法运行得很好,但是我刚刚对一个不同的类运行了一个测试,其中idgenerator类的一个实例被不同的线程连续调用了十次,每次都返回相同的值。日志显示,在每次调用时,包含自由范围的集合都有相同的内容,尽管lastid变量不同:第一次调用时为-1,其他调用时为0。这似乎表明不同的线程正在使用集合的不同副本,尽管这不是我从代码中所期望的。
我在Windows10上的EclipseNeono4.6.3中使用JRE1.8.0u191运行。
我尝试在生成器对象而不是集合上进行同步,将treeset包装在synchronizedsortedset中,并使用lock对象而不是synchronized关键字。这些都没什么区别。

private final SortedSet<Range> freeRanges = new TreeSet<>();
private int lastId;


public int allocateId() throws IllegalStateException
{
    int answer;
    synchronized (freeRanges)
    {
        LOG.debug("lastId = {}, freeRanges = {}", lastId, freeRanges);
        if (freeRanges.isEmpty())
            throw new IllegalStateException("All possible IDs are allocated");
        Range range = Stream
                .of(freeRanges.tailSet(new Range(lastId + 1)), freeRanges)
                .filter(s -> !s.isEmpty())
                .map(SortedSet::first)
                .findFirst()
                .get();
        answer = lastId = range.start;
        freeRanges.remove(range);
        if (range.start != range.end)
            freeRanges.add(new Range(range.start + 1, range.end));
        LOG.debug("Allocated {}, freeRanges = {}", answer, freeRanges);
    }
    return answer;
}

日志输出如下所示。我希望在第n次调用中,分配的数字是n-1,并且更新自由范围集,以显示从n开始到100结束的范围。但相反,我看到的是:
16:03:18.554 [main] DEBUG uk.org.thehickses.idgenerator.IdGenerator - lastId = -1, freeRanges = [Range [start=0, end=100]]
16:03:18.570 [main] DEBUG uk.org.thehickses.idgenerator.IdGenerator - Allocated 0, freeRanges = [Range [start=1, end=100]]
16:03:18.586 [main] DEBUG uk.org.thehickses.idgenerator.IdGenerator - lastId = 0, freeRanges = [Range [start=0, end=100]]
16:03:18.586 [main] DEBUG uk.org.thehickses.idgenerator.IdGenerator - Allocated 0, freeRanges = [Range [start=1, end=100]]
16:03:18.586 [main] DEBUG uk.org.thehickses.idgenerator.IdGenerator - lastId = 0, freeRanges = [Range [start=0, end=100]]
16:03:18.586 [main] DEBUG uk.org.thehickses.idgenerator.IdGenerator - Allocated 0, freeRanges = [Range [start=1, end=100]]
16:03:18.586 [main] DEBUG uk.org.thehickses.idgenerator.IdGenerator - lastId = 0, freeRanges = [Range [start=0, end=100]]
16:03:18.586 [main] DEBUG uk.org.thehickses.idgenerator.IdGenerator - Allocated 0, freeRanges = [Range [start=1, end=100]]
16:03:18.586 [main] DEBUG uk.org.thehickses.idgenerator.IdGenerator - lastId = 0, freeRanges = [Range [start=0, end=100]]
16:03:18.586 [main] DEBUG uk.org.thehickses.idgenerator.IdGenerator - Allocated 0, freeRanges = [Range [start=1, end=100]]
16:03:18.586 [main] DEBUG uk.org.thehickses.idgenerator.IdGenerator - lastId = 0, freeRanges = [Range [start=0, end=100]]
16:03:18.586 [main] DEBUG uk.org.thehickses.idgenerator.IdGenerator - Allocated 0, freeRanges = [Range [start=1, end=100]]
16:03:18.586 [main] DEBUG uk.org.thehickses.idgenerator.IdGenerator - lastId = 0, freeRanges = [Range [start=0, end=100]]
16:03:18.586 [main] DEBUG uk.org.thehickses.idgenerator.IdGenerator - Allocated 0, freeRanges = [Range [start=1, end=100]]
16:03:18.586 [main] DEBUG uk.org.thehickses.idgenerator.IdGenerator - lastId = 0, freeRanges = [Range [start=0, end=100]]
16:03:18.586 [main] DEBUG uk.org.thehickses.idgenerator.IdGenerator - Allocated 0, freeRanges = [Range [start=1, end=100]]
16:03:18.586 [main] DEBUG uk.org.thehickses.idgenerator.IdGenerator - lastId = 0, freeRanges = [Range [start=0, end=100]]
16:03:18.586 [main] DEBUG uk.org.thehickses.idgenerator.IdGenerator - Allocated 0, freeRanges = [Range [start=1, end=100]]
16:03:18.586 [main] DEBUG uk.org.thehickses.idgenerator.IdGenerator - lastId = 0, freeRanges = [Range [start=0, end=100]]
16:03:18.586 [main] DEBUG uk.org.thehickses.idgenerator.IdGenerator - Allocated 0, freeRanges = [Range [start=1, end=100]]

最佳答案

谢谢所有回复的人。我担心我可能误导了你——我昨晚骑车回家时意识到,问题与来自不同线程对allocateId方法的多次调用无关。我发布的日志清楚地表明,所有这些调用都是在同一个线程中进行的(称为“main”)。
今天早上,当我骑车上班时,我意识到了问题所在——其他线程正在调用freeId方法,该方法将id返回到freeRanges集合。尤其是,在下一次调用allocateId之前,将释放每个allocateId调用分配的id。这解释了为什么每次调用freeRangesallocateId都有相同的内容。
我做了一个简单的更改,以确保在发生这种情况时,如果找到的范围包含lastId + 1则这是分配的值,即使它不在范围的开头。当然,如果它不在该范围的起始处,则在freeRanges中该范围将替换为最多两个新的范围-一个包含范围中小于分配的数字的所有数字,另一个包含大于分配的数字的所有数字。这确保了我们尽可能循环浏览所有可用的号码,并且只有在没有大于最后一个分配号码的空闲号码时,我们才会返回到开头。
修改后的代码如下。显然我应该花更多的时间在自行车上,而不是在电脑前!

public int allocateId() throws IllegalStateException
{
    int answer;
    synchronized (freeRanges)
    {
        LOG.debug("Allocating: lastId = {}, freeRanges = {}", lastId, freeRanges);
        if (freeRanges.isEmpty())
            throw new IllegalStateException("All possible IDs are allocated");
        int nextId = lastId + 1;
        Range range = Stream
                .of(freeRanges.tailSet(new Range(nextId)), freeRanges)
                .filter(s -> !s.isEmpty())
                .map(SortedSet::first)
                .findFirst()
                .get();
        answer = lastId = range.contains(nextId) ? nextId : range.start;
        freeRanges.remove(range);
        range.splitAround(answer).forEach(freeRanges::add);
        LOG.debug("Allocated {}, freeRanges = {}", answer, freeRanges);
    }
    return answer;
}

09-15 23:56