这个线程安全的字节序列生成器有什么问题?

时间:2011-09-12 08:01:21

标签: java concurrency puzzle atomic

我需要一个字节生成器,它将生成从Byte.MIN_VALUE到Byte.MAX_VALUE的值。当它达到MAX_VALUE时,它应该从MIN_VALUE重新开始。

我使用AtomicInteger编写代码(见下文);但是,如果同时访问并且如果使用Thread.sleep()人为地减慢了代码,那么代码似乎没有正常运行(如果没有睡眠,它运行正常;但是,我怀疑它对于出现并发问题来说太快了)。

代码(添加了一些调试代码):

public class ByteGenerator {

    private static final int INITIAL_VALUE = Byte.MIN_VALUE-1;

    private AtomicInteger counter = new AtomicInteger(INITIAL_VALUE);
    private AtomicInteger resetCounter = new AtomicInteger(0);

    private boolean isSlow = false;
    private long startTime;

    public byte nextValue() {
        int next = counter.incrementAndGet();
        //if (isSlow) slowDown(5);
        if (next > Byte.MAX_VALUE) {
            synchronized(counter) {
                int i = counter.get();
                //if value is still larger than max byte value, we reset it
                if (i > Byte.MAX_VALUE) {
                    counter.set(INITIAL_VALUE);
                    resetCounter.incrementAndGet();
                    if (isSlow) slowDownAndLog(10, "resetting");
                } else {
                    if (isSlow) slowDownAndLog(1, "missed");
                }
                next = counter.incrementAndGet();
            }
        }
        return (byte) next;
    }

    private void slowDown(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
        }
    }
    private void slowDownAndLog(long millis, String msg) {
        slowDown(millis);
        System.out.println(resetCounter + " " 
                           + (System.currentTimeMillis()-startTime) + " "
                           + Thread.currentThread().getName() + ": " + msg);
    }

    public void setSlow(boolean isSlow) {
        this.isSlow = isSlow;
    }
    public void setStartTime(long startTime) {
        this.startTime = startTime;
    }

}

而且,测试:

public class ByteGeneratorTest {

    @Test
    public void testGenerate() throws Exception {
        ByteGenerator g = new ByteGenerator();
        for (int n = 0; n < 10; n++) {
            for (int i = Byte.MIN_VALUE; i <= Byte.MAX_VALUE; i++) {
                assertEquals(i, g.nextValue());
            }
        }
    }

    @Test
    public void testGenerateMultiThreaded() throws Exception {
        final ByteGenerator g = new ByteGenerator();
        g.setSlow(true);
        final AtomicInteger[] counters = new AtomicInteger[Byte.MAX_VALUE-Byte.MIN_VALUE+1];
        for (int i = 0; i < counters.length; i++) {
            counters[i] = new AtomicInteger(0);
        }
        Thread[] threads = new Thread[100];
        final CountDownLatch latch = new CountDownLatch(threads.length);
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(new Runnable() {
                public void run() {
                    try {
                        for (int i = Byte.MIN_VALUE; i <= Byte.MAX_VALUE; i++) {
                            byte value = g.nextValue();
                            counters[value-Byte.MIN_VALUE].incrementAndGet();
                        }
                    } finally {
                        latch.countDown();
                    }
                }
            }, "generator-client-" + i);
            threads[i].setDaemon(true);
        }
        g.setStartTime(System.currentTimeMillis());
        for (int i = 0; i < threads.length; i++) {
            threads[i].start();
        }
        latch.await();
        for (int i = 0; i < counters.length; i++) {
            System.out.println("value #" + (i+Byte.MIN_VALUE) + ": " + counters[i].get());
        }
        //print out the number of hits for each value
        for (int i = 0; i < counters.length; i++) {
            assertEquals("value #" + (i+Byte.MIN_VALUE), threads.length, counters[i].get());
        }
    }

}

我的2核机器的结果是值#-128得到146次点击(所有这些都应该得到100次点击,因为我们有100个线程)。

如果有人有任何想法,这个代码有什么问题,我都是耳朵/眼睛。

更新:对于那些赶时间并且不想向下滚动的人来说,在Java中解决这个问题的正确(以及最短和最优雅)方式将是这样的:

public byte nextValue() {
   return (byte) counter.incrementAndGet();
}

谢谢,亨氏!

6 个答案:

答案 0 :(得分:9)

最初,Java将所有字段存储为4或8字节值,甚至是短字节。对字段的操作只会进行位屏蔽以缩小字节。因此,我们可以很容易地做到这一点:

public byte nextValue() {
   return (byte) counter.incrementAndGet();
}

有趣的小谜题,谢谢Neeme: - )

答案 1 :(得分:5)

您根据旧的counter.get()值决定incrementAndGet()。在对计数器执行incrementAndGet()操作之前,计数器的值可以再次达到MAX_VALUE。

if (next > Byte.MAX_VALUE) {
    synchronized(counter) {
        int i = counter.get(); //here You make sure the the counter is not over the MAX_VALUE
        if (i > Byte.MAX_VALUE) {
            counter.set(INITIAL_VALUE);
            resetCounter.incrementAndGet();
            if (isSlow) slowDownAndLog(10, "resetting");
        } else {
            if (isSlow) slowDownAndLog(1, "missed"); //the counter can reach MAX_VALUE again if you wait here long enough
        }
        next = counter.incrementAndGet(); //here you increment on return the counter that can reach >MAX_VALUE in the meantime
    }
}

要使其工作,必须确保不对陈旧信息做出决定。重置计数器或返回旧值。

public byte nextValue() {
    int next = counter.incrementAndGet();

    if (next > Byte.MAX_VALUE) {
        synchronized(counter) {
            next = counter.incrementAndGet();
            //if value is still larger than max byte value, we reset it
            if (next > Byte.MAX_VALUE) {
                counter.set(INITIAL_VALUE + 1);
                next = INITIAL_VALUE + 1;
                resetCounter.incrementAndGet();
                if (isSlow) slowDownAndLog(10, "resetting");
            } else {
                if (isSlow) slowDownAndLog(1, "missed");
            }
        }
    }

    return (byte) next;
}

答案 2 :(得分:3)

您的同步块仅包含if正文。它应该包括整个方法,包括if语句本身。或者只是让方法nextValue同步。在这种情况下,BTW根本不需要原子变量。

我希望这对你有用。尝试仅在您真正需要最高性能代码时使用原子变量,即synchronized语句困扰您。恕我直言,在大多数情况下,它没有。

答案 3 :(得分:2)

如果我理解正确,您会关心nextValue的结果在Byte.MIN_VALUE和Byte.MAX_VALUE的范围内,并且您不关心计数器中存储的值。 然后,您可以在字节上映射整数,以便公开所需的枚举行为:

private static final int VALUE_RANGE = Byte.MAX_VALUE - Byte.MIN_VALUE + 1;
private final AtomicInteger counter = new AtomicInteger(0);

public byte nextValue() {
   return (byte) (counter.incrementAndGet() % VALUE_RANGE + Byte.MIN_VALUE - 1);
}

请注意,这是未经测试的代码。但这个想法应该有效。

答案 4 :(得分:1)

我使用nextValue编写了以下版本的compareAndSet,该版本旨在用于非同步块。它通过了你的单元测试:

哦,我为MIN_VALUE和MAX_VALUE引入了新常量,但如果您愿意,可以忽略它们。

static final int LOWEST_VALUE = Byte.MIN_VALUE;
static final int HIGHEST_VALUE = Byte.MAX_VALUE;

private AtomicInteger counter = new AtomicInteger(LOWEST_VALUE - 1);
private AtomicInteger resetCounter = new AtomicInteger(0);

public byte nextValue() {
    int oldValue; 
    int newValue; 

    do {
        oldValue = counter.get();
        if (oldValue >= HIGHEST_VALUE) {
            newValue = LOWEST_VALUE;
            resetCounter.incrementAndGet();
            if (isSlow) slowDownAndLog(10, "resetting");
        } else {
            newValue = oldValue + 1;    
            if (isSlow) slowDownAndLog(1, "missed");
        }
    } while (!counter.compareAndSet(oldValue, newValue));
    return (byte) newValue;
}

compareAndSet()get()一起使用来管理并发。

在关键部分的开头,执行get()以检索旧值。然后,您执行一些仅依赖于旧值的函数来计算新值。然后使用compareAndSet()设置新值。如果AtomicInteger不再等于执行compareAndSet()时的旧值(由于并发活动),则它会失败,您必须重新开始。

如果你有极高的并发性并且计算时间很长,可以想象compareAndSet()在成功之前可能会多次失败,如果你担心,可能值得收集统计数据。

我并不是像其他人所建议的那样建议这比简单的同步块更好或更差,但为了简单起见,我个人可能会使用同步块。

编辑:我会回答您的实际问题“为什么我的工作不合作?”

您的代码有:

    int next = counter.incrementAndGet();
    if (next > Byte.MAX_VALUE) {

由于这两行不受同步块保护,因此多个线程可以同时执行它们,并且所有线程都获得next&gt;的值。 Byte.MAX_VALUE。然后所有这些都将进入同步块并将counter设置回INITIAL_VALUE(彼此等待时一个接一个)。

多年来,有很多关于试图通过在没有必要的情况下不同步而进行性能调整的陷阱而写的数量巨大。例如,请参阅Double Checked Locking

答案 5 :(得分:0)

尽管Heinz Kabutz是对特定问题的明确答案,但旧的Java SE 8 [2014年3月]还是添加了AtomicIntger.updateAndGet(和朋友)。如果需要,这将导致更通用的解决方案:

public class ByteGenerator {
    private static final int MIN = Byte.MIN_VALUE;
    private static final int MAX = Byte.MAX_VALUE;

    private final AtomicInteger counter = new AtomicInteger(MIN);

    public byte nextValue() {
        return (byte)counter.getAndUpdate(ByteGenerator::update);
    }
    private static int update(int old) {
        return old==MAX ? MIN : old+1;
    } 
}