以原子方式将新字符连接到 StringBuilder 对象

Posted

技术标签:

【中文标题】以原子方式将新字符连接到 StringBuilder 对象【英文标题】:Concat new characters to StringBuilder object atomically 【发布时间】:2022-01-16 16:38:15 【问题描述】:

我的问题是:

我有课:

public class AtomicStringBuilder 
    private final AtomicReference<StringBuilder> sbRef;

我需要同时原子地向 StringBuilder 添加新字符。但问题是,这个对象中只能有最后 128 个字符。我不能使用 StringBuffer,因为操作应该是非阻塞的。

所以,有两个操作:

首先:检查 StringBuilder 是否已经有 128 个字符。

第二个:如果没有 -> 添加新字符,如果有 -> 删除第一个字符并添加新字符。

有没有办法让这两个或三个操作原子化?

我做了这个方法,但是不行:

public void append(String string) 
        this.sbRef.getAndUpdate(ref -> 
            if (ref.length() < 128) 
                ref.append(string);
             else 
                ref.append(string).delete(0, ref.length() - 128);
            
            return ref;
        );
    

为了测试我创建了这个方法:

public void test() 
AtomicStringBuilder atomicStringBuilder = new AtomicStringBuilder();
Random random = new Random();
Stream<Integer> infiniteStream = Stream.iterate(0, i -> random.nextInt(10));

infiniteStream.parallel()
.limit(100000)
.forEach(integer -> atomicStringBuilder.append(String.valueOf(integer)));

assertEquals(128, atomicStringBuilder.getSb().get().length());

这不是一个真正的问题,我可以用其他任何可行的方法来更改 AtomicReference。任务是创建无锁且没有竞争条件的操作

【问题讨论】:

制作append synchronized 我不能。正如我所说,操作应该是无锁的,所以“同步”是不允许的 这是作业还是现实世界的问题? AtomicReference 不起作用,因为您的函数使用副作用而不是返回新引用。 您甚至不能在没有锁定和竞争条件的情况下跨多个线程修改 StringBuilder。如果您尝试过,您将不会与该操作建立发生之前的关系。建立之前发生的两种主要方法是使用锁(您说您不能使用)或易失性写入(对于 StringBuilder 操作而言,这不是原子的,即使每个逻辑突变只有一个操作) .您是否必须使用 StringBuilder,或者您可以使用不可变字符串进行 compareAndSet 吗? 是的,是的;但如果你想要无锁,那真的是最好的方法。 可能还有其他可能性,但它们会非常复杂,最终可能会更慢和更麻烦。通常在使用多线程时,为了提高并行度,您必须对单线程性能造成一些影响。这几乎肯定是这样一种情况。 【参考方案1】:

这是一个使用不可变字符串的解决方案。

如果你使用AtomicReference,你需要返回一个新的引用而不是改变引用指向的对象。以原子方式比较引用的当前值和预期值是知道它没有被另一个线程更新的唯一方法。

getAndUpdate 这样做:

    获取当前参考 将 lambda 应用于引用,获得新的引用 如果当前引用没有改变,则自动将其设置为新引用,否则返回 1。
public class App 
    static class AtomicStringBuilder 
        public final AtomicInteger counter = new AtomicInteger();

        public final AtomicReference<String> sbRef = new AtomicReference<>("");

        public void append(String string) 
            this.sbRef.getAndUpdate(ref -> 
                counter.getAndIncrement();
                if (ref.length() < 128) 
                    return ref + string;
                 else 
                    String s = ref + string;
                    return s.substring(s.length() - 128);
                
            );
        
    

    static void test() 
        AtomicStringBuilder atomicStringBuilder = new AtomicStringBuilder();
        Random random = new Random();
        Stream<Integer> infiniteStream = Stream.iterate(0, i -> random.nextInt(10));

        infiniteStream.parallel()
                .limit(100000)
                .forEach(integer -> atomicStringBuilder.append(String.valueOf(integer)));

        if (128 != atomicStringBuilder.sbRef.get().length()) 
            System.out.println("failed ");
        
        System.out.println(atomicStringBuilder.sbRef.get());
        System.out.println(atomicStringBuilder.counter.get());
    

    public static void main(String[] args) 
        test();
    

我在 lambda 中添加了一个计数器。运行此程序后显示的值会超过 100,000,因为并发更新强制重试。

【讨论】:

getAndAccumulate 可能是一个更好的选择。您可以实现一个包私有String appendAndTruncate(String previous, String toAppend),您将单独对其进行单元测试,然后公共附加只是atomicRef.getAndAccumulate(string, this::appendAndTruncate) 感谢您的回答。你能给我解释一下原子中的 lambda 是如何工作的吗?它不是关于这个例子,而是一般来说。例如,有一个巨大的 lambda 并且只有在它的最后一个 CAS 操作。如果一个线程未能通过 CAS,它将进入一个循环。但它是通过孔 lambda,还是只通过 CAS 部分? @AlchemistAction Javadoc 告诉您:“该函数应该没有副作用,因为当尝试更新由于线程之间的争用而失败时,它可能会被重新应用。” lambda 本身没有 CAS:lambda 返回一个全新的值,如果 CAS 失败,该方法将获取最新值并重新应用 [whole] lambda。

以上是关于以原子方式将新字符连接到 StringBuilder 对象的主要内容,如果未能解决你的问题,请参考以下文章

SQL SERVER - 选择下一行值最多 5 个字符,然后用新字符替换第一个字符

如何在C中将char连接到char *?

如何以编程方式查找 Android 版本名称?

如何确保以原子方式完成 JDBC 批量插入?

Java compareAndSet 以原子方式更新 BST 中的引用

如何检查 Java 程序的输入/输出流是不是连接到终端?