我正在创建一个Java类IdGenerator,该类将在每次请求一个ID时分配一个唯一的整数ID。它使用TreeSet来存储可用ID的范围,每次请求ID时,它都会在集合中查找以找到范围,分配该范围中的第一个ID,删除该范围,并添加一个较小的新范围。整个分配过程在集合上同步,以确保不同的线程不会冲突。
当我对类进行单元测试时,这很好用,但是我刚刚运行了一个不同类的测试,其中IdGenerator类的实例被不同的线程快速连续调用十次,并且返回相同的值每次。记录显示,每次调用时,包含自由范围的集合具有相同的内容,尽管lastId变量不同:第一次调用为-1,其他调用为0。这似乎表明不同的线程正在使用该集合的不同副本,尽管这不是我期望的代码。
我正在Windows 10的Eclipse Neon 4.6.3中使用JRE 1.8.0_191运行。
我尝试在生成器对象而不是集合上进行同步,将TreeSet包裹在syncedSortedSet中,并使用Lock对象而不是synced关键字。没有一个有什么不同。
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]]
答案 0 :(得分:0)
感谢所有回复的人。恐怕我可能误导了您-我意识到,昨晚骑车回家时,问题与从不同线程多次调用allocateId
方法无关。我发布的日志清楚地表明,所有这些调用都是在相同线程(称为“主”)中进行的。
今天早上骑自行车上班时,我意识到问题出在-其他线程正在调用freeId
方法,该方法将ID返回到freeRanges
集。特别是,在下一次对allocateId
的调用之前 之前,释放了每次对allocateId
的调用分配的ID。这就解释了为什么每次调用freeRanges
时allocateId
具有相同的内容。
我进行了一个简单的更改,以确保在发生这种情况时,如果找到的范围包含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;
}