Java 多线程同步问题的探究(三、Lock来了,大家都让开【2. Fair or Unfair? It is a question...】)
2010-05-14 00:00:00 来源:WEB开发网让我们继续前面有关ReentrantLock的话题。
首先,ReentrantLock有一个带布尔型参数的构造函数,在JDK官方文档中对它是这样描述的:
“此类的构造方法接受一个可选的公平 参数。当设置为 true 时,在多个线程的争用下,这些锁倾向于将访问权授予等待时间最长的线程。否则此锁将无法保证任何特定访问顺序。与采用默认设置(使用不公平锁)相比,使用公平锁的程序在许多线程访问时表现为很低的总体吞吐量(即速度很慢,常常极其慢),但是在获得锁和保证锁分配的均衡性时差异较小。不过要注意的是,公平锁不能保证线程调度的公平性。因此,使用公平锁的众多线程中的一员可能获得多倍的成功机会,这种情况发生在其他活动线程没有被处理并且目前并未持有锁时。还要注意的是,未定时的 tryLock 方法并没有使用公平设置。因为即使其他线程正在等待,只要该锁是可用的,此方法就可以获得成功。 ”
简单来讲:公平锁使线程按照请求锁的顺序依次获得锁;而不公平锁则允许讨价还价,在这种情况下,线程有时可以比先请求锁的其他线程先得到锁。
观察采用公平锁和非公平锁的例程运行效果发现:线程获得锁的顺序发生了一些变化(见下表)。
Unfair: 1 is running! 1 got lock1@Step1! 3 is running! 2 is running! 1 first Reading count:1 1 release lock1@Step1! 3 got lock1@Step1! 1 got lock2@Step2! thread 1 set age to:18 thread 1 first read age is:18 3 first Reading count:2 3 release lock1@Step1! 2 got lock1@Step1! thread 1 second read age is:18 1 release lock2@Step2! 3 got lock2@Step2! thread 3 set age to:34 thread 3 first read age is:34 2 first Reading count:3 2 release lock1@Step1! thread 3 second read age is:34 3 release lock2@Step2! 2 got lock2@Step2! thread 2 set age to:72 thread 2 first read age is:72 thread 2 second read age is:72 2 release lock2@Step2! 成功生成(总时间:20 秒) | Fair: 1 is running! 1 got lock1@Step1! 2 is running! 3 is running! 1 first Reading count:1 1 release lock1@Step1! 1 got lock2@Step2! thread 1 set age to:82 thread 1 first read age is:82 2 got lock1@Step1! 2 first Reading count:2 2 release lock1@Step1! 3 got lock1@Step1! thread 1 second read age is:82 1 release lock2@Step2! 2 got lock2@Step2! thread 2 set age to:65 thread 2 first read age is:65 3 first Reading count:3 3 release lock1@Step1! thread 2 second read age is:65 2 release lock2@Step2! 3 got lock2@Step2! thread 3 set age to:31 thread 3 first read age is:31 thread 3 second read age is:31 3 release lock2@Step2! 成功生成(总时间:20 秒) |
这样的变化告诉我们:
采用非公平的锁时,当一个线程释放了第一个锁以后,由于线程的抢占,刚刚被释放的锁马上被下一个线程占有。采用公平锁时,由于公平锁倾向于将访问权授予等待时间最长的线程,所以,当第一个锁被第一个线程释放以后,第二个锁马上将访问权授予第一个线程,而第一个锁将访问权授予了第二个线程。这里,公平锁在平衡分配方面耗费了一定的时间,这使得第一个线程获得第二个锁的时间优先于第二个线程获得第一个锁。这样,采用不同的锁,就出现了两种不同的结果。
为了看到公平锁和非公平锁性能上的差异,我们不妨将其中线程的睡眠时间设定为1毫秒,然后把循环产生的线程数提高到5000(修改后的代码已忽略,自行修改),可以发现,由于公平锁要维持锁分配的均衡性,所以,采用公平锁的程序总运行时间更长一些。
根据运行环境的差异,有些朋友可能并不一定能很直观的从运行结果中看到两种不同的锁带来的性能差异。不妨引用IBM开发者社区的一组测试结果来看一看就行有什么样的差异吧:
4CPU情况下的同步、非公平锁和公平锁吞吐量比较:
查看原图(大图)
单CPU情况下,同步、非公平锁和公平锁的吞吐量:
查看原图(大图)
可以看到,同步和公平锁的吞吐量都是最低的,公平锁更低一些。但是同步内置的监控器锁是不公平的,而且永远都是不公平的。而JVM 保证了所有线程最终都会得到它们所等候的锁。确保统计上的公平性,对多数情况来说,这就已经足够了,而这花费的成本则要比绝对的公平保证的低得多。
既然Lock这么近乎完美,那我们也许可以忘却synchronized了。
但是任何事物都是有两面性的。
1.使用Lock,你必须手动的在finally块中释放锁。锁的获得和释放是不受JVM控制的。这要求编程人员更加细心。
2.当 JVM 用 synchronized 管理锁定请求和释放时,JVM 在生成线程转储时能够包括锁定信息。这些对调试非常有价值,因为它们能标识死锁或者其他异常行为的来源。 Lock 类只是普通的类,JVM 不知道具体哪个线程拥有 Lock 对象。
Lock提供了在多线程争用的情况下更好的并发性,但这是以牺牲一定的可维护性为代价的。
所以说,当大量线程发生争用的时候,Lock来了,大家都让开。
更多精彩
赞助商链接