Java中的volatile和synchronized关键字

4
我有一个静态方法,它应该根据当前时间戳生成唯一的ID,如下所示。为了确保新生成的ID不与先前生成的ID相同(由于计算机非常快,毫秒数不会改变),我使用循环比较新生成的ID和先前生成的ID。如果它们相同,它将生成另一个ID。
public class Util {

    protected static String uniqueID;

    public static String generateUniqueID() {
        SimpleDateFormat timstampFormat = new SimpleDateFormat("yyyyMMddHHmmssSSS");
        do {
            String timestamp = timstampFormat.format(new Date());
            if (!timestamp.equals(uniqueID)) {
                uniqueID = timestamp;
                return uniqueID;
            }
        } while (true);
    }

}

当多线程调用该方法时,我希望上述代码能够正常工作。

如果我只是将volatile关键字放到uniqueID变量中,是否已足够?我是否仍需要有同步块?

如果有同步块但没有volatile关键字呢?

先行致谢。

添加:

如果我更改为下面的代码,那么volatile关键字是否仍然需要?

public class Util {

    private static volatile String uniqueID;

    public static synchronized String generateUniqueID() {
        uniqueID = UUID.randomUUID().toString();
        return uniqueID;
    }

}

4
这太糟糕了!你所做的是把一台“非常快速的计算机”变得荒谬缓慢。不仅每次调用都创建一个 “SimpleDateFormat”(你可以使用池,或者在Java 8中简单地使用不可变且线程安全的 DateTimeFormatter),而且还会出现繁忙等待。这段代码唯一可以做的事情就是把强大的计算机变成低效的风扇加热器。不要这样做,任何一个方面都不要这样做。 - Boris the Spider
1
我建议你开始阅读一些教程,例如Oracle并发课程。目前为止,你只是在使用一些术语而没有理解它们的含义。对于你的问题:volatile不足以,我会同步方法。 - Turing85
1
@Turing85,就目前而言,最好完全放弃线程。当前的代码使用synchronized,除了使并发代码变成同步代码之外,几乎没有其他作用。锁争用会让一个成年人哭泣... - Boris the Spider
2
使用UUID代替自己的代码。 - Antoniossss
1
重要的是要意识到,如果您坚持使用自己的ID格式,无论您使用繁忙等待、休眠或魔法棒等什么代码,您都无法在每毫秒内提供多个ID。每毫秒只有一个ID,只有一个调用者会获得它。 - RealSkeptic
显示剩余5条评论
4个回答

7
这段代码存在多个问题。
首先,绝对不要使用时间戳作为UID,除非你完全确定,在你使用的时间戳最低分辨率内不会生成多个UID。我建议采用完全不同的方法。如果你一定要保持时间戳格式,则将计数器附加到时间戳后面,或者直接使用计数器。另外一个选择是,除了正常系统时间之外,还使用System.nanoTime(),但这种方法可能会产生一些问题。
如果你试图在同一毫秒内生成两个UID,你的while循环将循环最多一个整毫秒。这不需要快速计算机就能占用大量CPU时间。循环至少会运行数千次才能得到正确结果。
将变量标记为volatile并不能解决问题。你必须将整个方法中运行的块标记为synchronized,以防止多个线程同时运行它。但考虑一个情况,在一个毫秒内你想生成1000个UID。现在本该立即完成的任务却需要1秒钟完成。这创建了一个巨大的瓶颈。
我的建议: 立即删除此方法。没有什么方法可以使此代码的性能和准确性达到可以接受的水平。请阅读关于并发性的这篇教程,获取一种新的生成UID的方法,并从头开始。
或者: 为什么要编写已经存在的东西呢?使用Oracle提供的UID-class。另一个好方法是使用UUID,它是实用程序包的一部分,很可能比UID更通用。这取决于你对生成的UID的要求。

1
是的。它实际上是为与服务器一起使用而实现的。 - user4668606

2
你有一个问题,就是你正在忙碌地等待每一毫秒,如果有多个线程在等待,它们都将等待,可能会无休止地等待。这将发生在您提供线程安全的方式如何。更好的方法是使用一个计数器,该计数器始终大于或等于时间,或者使用一个计数器,该计数器是当前时间的多倍。
你可以这样做:
private static final AtomicLong time = new AtomicLong(0);
public static long uniqueTimedId() {
    while(true) {
         long now = System.currentTimeMillis();
         long value = time.get();
         long next = now > value ? now : value + 1;
         if (time.compareAndSwap(value, next))
            return next;
    }
}

这将为您提供每毫秒的id,无需等待。当您在同一毫秒内有多个id时,有些将在未来。如果您想要,可以将其转换为您的格式字符串。但是,这仍然存在一个问题,即每毫秒不能超过1个,否则会与当前时间偏离。

private static final AtomicLong time = new AtomicLong(0);
public static long uniqueTimedId() {
    while(true) {
         long now = System.currentTimeMillis() * 1000;
         long value = time.get();
         long next = now > value ? now : value + 1;
         if (time.compareAndSwap(value, next))
            return next;
    }
}

这将给你一个独特的ID,是当前时间的1000倍。这意味着你每毫秒可以有1000个无漂移的ID,当你将其转换为字符串时,需要除以1000来获取毫秒数,并在结尾处添加 x%1000 作为三位数。
一个简单的折中方案可能是乘以10。这将使您每毫秒获得10个ID。
同步块使用由CPU实现的读/写内存屏障来确保发生这种情况。
注意:如果使用synchronized,则不需要使用volatile。事实上,同时使用两者可能会更慢。
需要 synchronized ,因为您正在使用共享的变量。 volatile 是多余的,没有帮助。
private static String uniqueID;

public static synchronized String generateUniqueID() {
    uniqueID = UUID.randomUUID().toString();
    // without synchronized, one thread could read what another thread wrote
    return uniqueID;
}

如果你使用局部变量,就不需要使用synchronized或volatile,因为没有共享。

// nothing is shared, so no thread safety issues.
public static String generateUniqueID() {
    return UUID.randomUUID().toString();
}

1
不应该是 long next = now > value ? now : value + 1 吗? - olsli

1

和别人一样,我强烈建议摆脱繁忙循环、时间戳作为标识符以及同步易变的思维方式。易变不起作用,同步方式虽然可行但效率极低。

然而,我建议

  • 使用AtomicReference的compareAndSet进行检查,因为它能够使用CPU支持的实际原子操作
  • 使用不受时间限制的标识符,这样就可以避免等待时间变化的瓶颈
  • 如果您坚持使用时间戳作为标识符,请至少sleep等待!

1

为确保所有线程都能看到最新的修改,您的字段uniqueID必须是volatile的,并且您应该依赖于UUID(代表通用唯一标识符)来生成唯一的ID(即使在集群中也将是唯一的,因为它依赖于日期时间和MAC地址),您的代码将如下所示:

public static String generateUniqueID() {
    return UUID.randomUUID().toString();
}

public static UUID randomUUID()

静态工厂方法,用于获取类型为4的(伪随机生成的)UUID。该UUID使用加密强度的伪随机数生成器生成。


嗨,看一下我上面的编辑。所以仍然需要使用volatile来确保所有线程都能看到最新的修改? - user3573403
@user3573403 是的,它仍然是必需的。 - Nicolas Filotto
我不明白你的意思。如果实例变量通过同步块进行修改/访问,这是否意味着所有线程都可以看到最新的修改。为什么? - user3573403
1
@user3573403 因为这就是规则。并发编程不是你可以通过试错有效学习的东西(大多数事情你可以,但这个不同)。如果你对何时何地放置同步块以及它与易失性字段的关系感到不确定,那么你真的需要找一本书来解释Java并发编程的设计原理。如果你想编写相对无误的并发代码,那么你必须绝对理解基本原则。 - biziclop
是的,阅读这篇文章 - Nicolas Filotto
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接