多线程多锁似乎比单锁慢

Posted

技术标签:

【中文标题】多线程多锁似乎比单锁慢【英文标题】:Multithreading multiple locks seem slower than single lock 【发布时间】:2021-09-23 01:54:57 【问题描述】:

我正在阅读 Java 中的多线程和使用同步块。假设我有两个不同的、独立的同步块。我可以让它们并行运行,方法是为两个同步块分别使用一个锁。但是如果我对两个同步块使用相同的锁,我认为只有一个可以在给定时间运行。 我这样想错了吗?如果不是,为什么我会得到以下奇怪的结果?

假设我有两个独立的操作,increment1 和 increment2,分别由不同的线程调用。

public class AppMultipleSynchronization 

    private static int counter1 = 0;
    private static int counter2 = 0;

    private static Object lock1 = new Object();
    private static Object lock2 = new Object();

    public static void increment1() 
        synchronized (lock1) 
            counter1++;
        
    

    public static void increment2() 
        synchronized (lock2) 
            counter2++;
        
    

    public static void main(String[] args) 

        Thread t1 = new Thread(new Runnable() 
            @Override
            public void run() 
                for (int i = 0; i < 100000000; i++) 
                    increment1();
                
            
        );

        Thread t2 = new Thread(new Runnable() 
            @Override
            public void run() 
                for (int i = 0; i < 100000000; i++) 
                    increment2();
                
            
        );

        long startTime = System.currentTimeMillis();

        t1.start();
        t2.start();

        try 
            t1.join();
            t2.join();
         catch (InterruptedException e) 
            e.printStackTrace();
        

        long endTime = System.currentTimeMillis();

        System.out.println("total time taken: " + (endTime - startTime) + "ms");
        System.out.println("counter1: " + counter1 + "\ncounter2: " + counter2);
    

如您所见,我对两个增量都使用了不同的锁。 要使用相同的锁,请使用完全相同的程序,在两种情况下都替换为lock1

两个锁时的输出:

    total time taken: 13437ms
    counter1: 100000000
    counter2: 100000000

单锁情况下的输出:

    total time taken: 5139ms
    counter1: 100000000
    counter2: 100000000

【问题讨论】:

您有多少个处理器?尝试找出所花费的时间,而不是 synchronized(AppMultipleSynchronization.class) 使用 synchronized(lock1) 使用 JMH,假设我设置正确,我看不到平均执行时间有如此显着的差异:一个锁,487.514 ± 22.413 ms/op;两把锁,1064.114 ± 24.043 ms/op。执行时间只增加了半秒多一点。注意我循环了一千万次而不是一亿次。 @Gunwant 我试过 lock1 而不是 AppMultipleSynchronization.class..但结果还是一样... 有太多的因素需要通过实验来弄清楚......首先,您要在非最终字段上进行同步。尽管关于 final 对同步块的作用的描述只提到了安全性并且您没有更改值,但在我的测试中添加 final 将时间减少了一半。其次,必须进行一些大的优化——如果我将 main 方法的代码放在一个循环中,不同迭代所花费的时间非常不一致,有时会长 10 倍。第三,对我来说,在本地使用两个锁的方法比在 lock1 上都快。 几乎每次出现“Java 的性能不如我预期”这样的问题时,正确答案都是your benchmark is wrong。用 Java 编写正确的微基准测试很难,如果您至少不为此使用现有工具/库,那么您没有考虑所有可能的陷阱的可能性非常高。 【参考方案1】:

我这样想错了吗?如果没有,为什么我会得到以下奇怪的结果?

不,你没有错。如果您使用 2 个锁,则 2 个线程可以同时执行 2 个不同的块。

您所看到的很可能是因为这些细粒度、线程化的性能测试非常微妙。这可能与分支和锁定预测有关,而不是真正的运行时。 JVM 会猜测是否应该在自旋循环中测试锁与真正的互斥等待/队列。鉴于实际工作只是 ++,它可以显着优化此代码。也可能是t1.start() 启动并运行得如此之快,以至于它在t2.start() 可以执行之前完成。

如果您将锁体更改为更大,您应该开始看到 2 个锁将导致更快的挂钟运行时间。例如,如果您在块中执行 ++ 操作的循环:

public static void increment1() 
    synchronized (lock1) 
        for (int i = 0; i < 100000000; i++) 
            counter1++;
        
    

...
public void run() 
    for (int i = 0; i < 10000; i++) 
        increment1();
    

然后我得到 1 把锁:

total time taken: 62822ms
counter1: -727379968
counter2: -727379968

但是获得了 2 把锁:

total time taken: 30902ms
counter1: -727379968
counter2: -727379968

最后,JVM 在运行时的最初几秒甚至几分钟内执行了大量的类加载、gcc -O3 等效和机器代码内联。每当您尝试运行 Java 性能测试时,都需要确保您的程序运行了很长时间才能获得一定程度的准确数字。

【讨论】:

内部循环可以通过 JIT 完全优化。它可以被替换,例如“同步(..)counter+=100000000;” 可能是,是的,但在这种情况下似乎证明了这一点。 @pveentjer 你试过了吗?

以上是关于多线程多锁似乎比单锁慢的主要内容,如果未能解决你的问题,请参考以下文章

多线程 - 比单线程慢

由于 CPU 类型,C++ Boost 多线程比单线程慢?

多线程比单线程慢

C++11 多线程比单线程慢

多线程函数性能比单线程差

多线程并发一定比单线程快吗?