繁体   English   中英

Java - 同步方法导致程序大规模减速

[英]Java - Synchronized methods causes program to slow down massively

我正在尝试了解线程和同步。 我做了这个测试程序:

public class Test {
    static List<Thread> al = new ArrayList<>();

    public static void main(String[] args) throws IOException, InterruptedException {
        long startTime = System.currentTimeMillis();

        al.add(new Thread(() -> fib1(47)));
        al.add(new Thread(() -> fib2(47)));

        for (Thread t : al)
            t.start();
        for (Thread t: al)
            t.join();

        long totalTime = System.currentTimeMillis() - startTime;
        System.out.println(totalTime);
    }

    public static synchronized int fib1(int x) {
        return x <= 2 ? 1 : fib1(x-2) + fib1(x-1);
    }

    public static synchronized int fib2(int x) {
        return x <= 2 ? 1 : fib2(x-2) + fib2(x-1);
    }
}

这个程序需要大约273秒才能完成,但如果我删除了两个synchronized它,它将在7秒内完成。 是什么导致这种巨大的差异

编辑:我知道我正在使用一种非常慢的算法来计算斐波纳契数。 而且我也知道线程不共享资源,因此这些方法不需要同步。 然而,这只是一个测试程序,我试图弄清楚synchronized是如何工作的,我选择了一个慢速算法,所以我可以测量所用的时间,以毫秒为单位。

当你将一个static synchronized放在一个方法上时,为了让一个线程执行该方法,它首先必须获取该类的锁(这里是Test)。 两个静态fib方法使用相同的锁。 一个线程获取锁定,执行fib方法,并释放锁定,然后另一个线程获取执行该方法。 哪个线程首先获得锁定取决于操作系统。

已经提到过锁是可重入的,并且递归调用synchronized方法没有问题。 该线程在第一次调用fib方法时保持锁定,该调用在所有递归调用完成之前不会完成,因此该方法在线程释放锁之前运行完成。

主线程除了等待之外没有做任何事情,并且只有一个调用fib方法的线程可以一次运行。 删除synchronized修饰符会加快速度,而不会锁定两个线程可以同时运行,可能使用不同的处理器。

这些方法不会修改任何共享状态,因此没有理由同步它们。 即使它们确实需要同步,仍然没有理由在这里使用两个单独的fib方法,因为无论如何调用fib1或fib2方法都需要获取相同的锁。

使用synchronized without static意味着将对象实例(而不是类)用作锁。 所有同步方法使用相同锁的原因是关键是保护共享状态,对象可能有各种方法来修改对象的内部状态,并且保护该状态不受并发修改的影响,应该只执行一个线程这些方法中的任何一种。

你的程序不会卡住 - 它只是非常慢。 这是由于两个原因:

1.算法复杂性

正如其他人和你自己提到的那样,计算斐波纳契数的方式非常慢,因为它一遍又一遍地计算相同的值。 使用较小的输入会将运行时间降低到合理的值。 但这不是你的问题所在。

2.同步

这会以两种方式减慢您的程序:

首先,不需要使方法synchronized因为它们不会修改方法本身之外的任何内容。 实际上,它会阻止两个线程同时运行,因为这些方法是static因此防止两个线程同时出现在它们中的任何一个中。 所以你的代码实际上只使用一个线程,而不是两个。

synchronized也会给方法带来很大的开销,因为它在进入方法时需要获取锁 - 或者至少检查当前线程是否已经拥有锁。 这些操作非常昂贵,并且必须在每次输入其中一种方法时完成这些操作。 由于 - 由于递归 - 这种情况发生了很多 ,它对程序性能产生了极大的影响。

有趣的是,当您只使用一个线程运行它时性能会好得多 - 即使方法是synchronized 原因是JVM完成了运行时优化。 如果您只使用一个线程,则JVM可以优化synchronized检查,因为不会发生冲突。 这大大减少了运行时间 - 但不完全是由于以“冷代码”和一些剩余的运行时检查开始而没有synchronized的值。 另一方面,当使用2个线程运行时,JVM无法执行此优化,因此会留下导致代码速度非常慢的昂贵的synchronized操作。

顺便说一下:fib1和fib2相同,删除其中一个

@MartinS是正确的,因为你没有共享状态,所以不需要synchronized。 也就是说,没有数据是您试图阻止多个线程同时访问的数据。

但是,您通过添加同步调用来减慢程序速度。 我的猜测是,如果没有同步,你应该看到两个核心以100%旋转,但计算此方法需要多长时间。 当您添加synchronized时,无论哪个线程抓取,锁定首先会以100%旋转。 另一个坐在那里等待锁定。 当第一个线程完成时,第二个线程就会完成。

您可以通过计算程序来测试它(从较小的值开始,以使其保持合理的时间)。 该程序应该在大约一半的时间内运行,而不是同步。

您的程序没有死锁,并且由于不必要的同步,它也不会明显变慢。 由于递归函数的分支因素 ,您的程序似乎“卡住”了。

递归的分支因子

N> = 4时 ,你递归两次。 换句话说,平均而言,递归的分支因子为2,这意味着如果您递归计算第N个Fibonacci数,则将调用函数大约2 ^ N次。 2 ^ 47是一个巨大的数字(就像数百万亿)。 正如其他人所建议的那样,您可以通过保存中间结果并返回它们而不是重新计算它们来减少这个数字。

更多关于同步

获取锁昂贵的。 但是,在Java中,如果一个线程有一个锁并重新输入它已拥有锁的同一个同步块,则它不必重新获取锁。 由于每个线程已经为它们输入的每个函数拥有相应的锁,因此它们只需要在程序的持续时间内获得一个锁。 获得一把锁的成本与递增数百万亿次相比是微不足道的:)

fib1 (或fib2 )方法再次出现时,它不会释放锁定。 更重要的是,它再次获得锁定(它比初始锁定更快)。 好消息是Java中的synchronized方法是可重入的。

最好不要同步递归本身。

将递归方法拆分为两个:

  • 一个递归的非同步方法(它应该是private因为它不是线程安全的);
  • 一个没有递归本身的 public 同步方法,它调用第二个方法。

尝试测量这样的代码,你应该得到14秒,因为两个线程在同一个锁Test.class上同步。

您看到的问题是因为静态同步方法在上同步。 因此,你的两个主题花费了大量的时间来争夺Test.class.上的单个锁Test.class.

出于学习练习的目的,加快速度的最佳方法是创建两个显式锁定对象。 在测试中,添加

static final Object LOCK1 = new Object();
static final Object LOCK2 = new Object();

然后,在fib1()fib2() ,对这两个对象使用synchronized块。 例如

public static int fib1(int x) {
   synchronized(LOCK1) {
        return x <= 2 ? 1 : fib1(x-2) + fib1(x-1);
    }
}

public static int fib2(int x) {
   synchronized(LOCK2) {
        return x <= 2 ? 1 : fib2(x-2) + fib2(x-1);
    }
}

现在第一个线程只需要抓住LOCK1 ,没有争用,第二个线程只能抓住LOCK2 ,没有争用。 (只要你只有这两个线程)这应该只比完全不同步的代码稍慢。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM