我正在尝试了解线程和同步。我做了这个测试程序:
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
是如何工作的,我会故意选择一个慢速算法,所以我可以用毫秒来测量时间。
答案 0 :(得分:2)
当你将static synchronized
置于一个方法上时,为了让一个线程执行该方法,它首先必须获取该类的锁(这里是Test)。两个静态fib方法使用相同的锁。一个线程获取锁定,执行fib方法,并释放锁定,然后另一个线程获取执行该方法。哪个线程首先获得锁定取决于操作系统。
已经提到过锁是可重入的,并且递归调用synchronized方法没有问题。该线程在第一次调用fib方法时保持锁定,该调用在所有递归调用完成之前不会完成,因此该方法在线程释放锁之前运行完成。
主线程除了等待之外什么也没做,只有一个调用fib方法的线程可以一次运行。删除synchronized修饰符会加快速度,而不会锁定两个线程可以同时运行,可能使用不同的处理器。
这些方法不会修改任何共享状态,因此没有理由同步它们。即使它们确实需要同步,仍然没有理由在这里使用两个单独的fib方法,因为无论如何调用fib1或fib2方法都需要获取相同的锁。
使用synchronized without static意味着将对象实例(而不是类)用作锁。所有同步方法使用相同锁的原因是关键是保护共享状态,对象可能有各种方法来修改对象的内部状态,并保护该状态不受并发修改的影响,应该只执行一个线程这些方法中的任何一种。
答案 1 :(得分:1)
你的程序不会卡住 - 它的速度非常慢。 这是由于两个原因:
<强> 1。算法复杂度
正如其他人和你自己提到的那样,计算斐波纳契数的方式非常慢,因为它反复计算相同的值。使用较小的输入会将运行时间降低到合理的值。但这不是你的问题所在。
<强> 2。同步
这会以两种方式减慢你的程序:
首先,制作方法synchronized
不是必需的,因为它们不会修改方法本身之外的任何内容。实际上,它会阻止两个线程同时运行,因为方法是static
,因此防止两个线程同时位于其中任何一个线程中。
所以你的代码实际上只使用一个线程,而不是两个。
同样synchronized
会给方法增加很大的开销,因为它需要在输入方法时获取锁定 - 或者至少检查当前线程是否已经拥有锁定。
这些操作非常昂贵,并且必须在每次输入其中一种方法时完成这些操作。由于 - 由于递归 - 发生很多,它对程序性能有极大的影响。
有趣的是,当您使用单个线程运行它时,性能会好得多 - 即使方法为synchronized
。
原因是JVM完成了运行时优化。
如果您只使用一个线程,JVM可以优化synchronized
检查,因为不存在冲突。这大大减少了运行时间 - 但不完全是因为没有synchronized
因为“冷代码”和一些剩余的运行时检查而没有synchronized
。
另一方面,当使用2个线程运行时,JVM无法执行此优化,因此留下了导致代码速度非常慢的昂贵的var outfit = 26; //this is the same as 2 & 8 & 16
var bIsWearingPants = ((outfit | 8) != 0);
操作。
顺便说一下:fib1和fib2相同,删除其中一个
答案 2 :(得分:0)
@MartinS是正确的,因为您没有共享状态,因此此处不需要synchronized。也就是说,没有数据可以阻止多个线程同时访问。
但是,通过添加同步调用,您的节目速度会降低。我的猜测是,如果没有同步,你应该看到两个核心以100%旋转,但计算此方法需要多长时间。当您添加synchronized时,无论哪个线程抓取,锁定首先会以100%旋转。另一个坐在那里等待锁定。当第一个线程完成时,第二个线程就会完成。
你可以通过计算你的程序来测试它(从较小的值开始,以保持合理的时间)。该程序应该在大约一半的时间内运行,而不是同步。
答案 3 :(得分:0)
您的程序没有死锁,并且由于不必要的同步,它也不会明显变慢。由于递归函数的分支因子,您的程序似乎“卡住了”。
递归的分支因子
当 N&gt; = 4 时,你递归两次。换句话说,平均来说,递归的分支因子为2,这意味着如果你递归地计算 N -th Fibonacci数,你将调用你的函数大约2 ^ N 次。 2 ^ 47是一个巨大的数字(就像数百万亿)。正如其他人所建议的那样,您可以通过保存中间结果并返回它们而不是重新计算它们来减少这个数字。
有关同步的更多信息
获取锁 非常昂贵。但是,在Java中,如果一个线程有一个锁并重新输入它已拥有锁的同一个同步块,则它不必重新获取锁。由于每个线程已经为它们输入的每个函数拥有相应的锁,因此它们只需要在程序的持续时间内获得一个锁。获得一把锁的成本与递增数百万亿次相比是微不足道的:)
答案 4 :(得分:0)
当fib1
(或fib2
)方法重复出现时,它不会释放锁定。更重要的是,它再次获得锁定(它比初始锁定更快)。
好消息是Java中的synchronized
方法是可重入的。
最好不要同步递归本身。
将递归方法拆分为两个:
private
,因为它不是线程安全的); public
同步方法没有递归本身,它调用第二种方法。尝试测量这样的代码,你应该得到14秒,因为两个线程在同一个锁Test.class
上同步。
答案 5 :(得分:0)
您看到的问题是因为静态同步方法在类上同步。所以你的两个主题花费了大量的时间来争夺Test.class.
出于本次学习练习的目的,加快速度的最佳方法是创建两个显式锁定对象。在测试中,添加
static final Object LOCK1 = new Object();
static final Object LOCK2 = new Object();
然后,在fib1()
和fib2()
中,对这两个对象使用synchronized块。 e.g。
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
,没有争用。 (只要你只有这两个线程)这应该只比完全不同步的代码稍慢一点。