锁的公平和非公平
简单介绍:
在锁的获取上,存在一个公平性和不公平性的问题,所谓的公平是指,在一段时间内,先对锁发起获取请求的一定被先满足。或者可以理解成期望获取锁的线程是一个先进先出的队列,等待时间最久的线程最优先获取到锁。而非公平是指,获取锁的顺序并不是有序的,可以随时插队。就好比都在排队检票,突然来了一个人说他的火车马上就要开车了,想插在你前面检票进站,这时候你就有可能仁慈了,把位置让他插入了。 这时候,这个人就优先获取到了锁。 但这个并不公平。
性能测试:
两种锁的获取方式性能不一样,一般情况下公平的锁机制比非公平的效率低,因为公平的锁机制没有考虑到操作系统对线程的调度,会造成线程的上下文切换次数增加。(还有一种比较专业的说法:因为公平的获取锁没有考虑到操作系统对线程的调度因素,这样造成JVM对于等待中的线程调度次序和操作系统对线程的调度之间的不匹配)。具体看下下面的一个测试结果:
测试代码:
public class LockDemo {
private static CountDownLatch count = new CountDownLatch(5);
public static void main(String[] args) {
long start = System.currentTimeMillis();
testock(true);
// testock(false);
while (true){
if (count.getCount() == 0){
System.out.println((System.currentTimeMillis() - start) + "ms");
System.exit(1);
}
}
}
private static void testock(boolean fair) {
final Lock lock = new ReentrantLock2(fair);
System.out.println("cur lock version : fair?" + fair);
for (int i = 0; i < 5; i++) {
Thread t = new Thread(new Job(lock));
t.setName("" + i);//设置线程名称
t.start();
}
}
private static class Job implements Runnable {
Lock lock;
public Job(Lock lock) {
this.lock = lock;
}
@Override
public void run() {
for (int i = 0; i < 5; i++){
lock.lock();
try {
System.out.println("Locked by " + Thread.currentThread().getName() + ", waited "
+ ((ReentrantLock2) lock).getQuenedThreads());
try {
TimeUnit.MICROSECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
lock.unlock();
}
}
count.countDown();
}
}
private static class ReentrantLock2 extends ReentrantLock {
private static final long serialVersionUID = -5229790810454946297L;
public ReentrantLock2(boolean b) {
super(b);
}
//得到当前等待队列中的线程名称
public List
List
Collection
Iterator
while (it.hasNext()){
threadList.add(it.next().getName());
}
return threadList;
}
}
}
测试结果:
使用公平锁竞争机制时:
cur lock version : fair?true
Locked by 0, waited [1]
Locked by 1, waited [0, 4, 3, 2]
Locked by 2, waited [1, 0, 4, 3]
Locked by 3, waited [2, 1, 0, 4]
Locked by 4, waited [3, 2, 1, 0]
Locked by 0, waited [4, 3, 2, 1]
Locked by 1, waited [0, 4, 3, 2]
Locked by 2, waited [1, 0, 4, 3]
Locked by 3, waited [2, 1, 0, 4]
Locked by 4, waited [3, 2, 1, 0]
Locked by 0, waited [4, 3, 2, 1]
Locked by 1, waited [0, 4, 3, 2]
Locked by 2, waited [1, 0, 4, 3]
Locked by 3, waited [2, 1, 0, 4]
Locked by 4, waited [3, 2, 1, 0]
Locked by 0, waited [4, 3, 2, 1]
Locked by 1, waited [0, 4, 3, 2]
Locked by 2, waited [1, 0, 4, 3]
Locked by 3, waited [2, 1, 0, 4]
Locked by 4, waited [3, 2, 1, 0]
Locked by 0, waited [4, 3, 2, 1]
Locked by 1, waited [4, 3, 2]
Locked by 2, waited [4, 3]
Locked by 3, waited [4]
Locked by 4, waited []
使用非公平锁竞争机制时:
cur lock version : fair?false
Locked by 0, waited []
Locked by 0, waited [4, 3, 2, 1]
Locked by 0, waited [4, 3, 2, 1]
Locked by 0, waited [4, 3, 2, 1]
Locked by 0, waited [4, 3, 2, 1]
Locked by 1, waited [4, 3, 2]
Locked by 1, waited [4, 3, 2]
Locked by 1, waited [4, 3, 2]
Locked by 1, waited [4, 3, 2]
Locked by 1, waited [4, 3, 2]
Locked by 2, waited [4, 3]
Locked by 2, waited [4, 3]
Locked by 2, waited [4, 3]
Locked by 2, waited [4, 3]
Locked by 2, waited [4, 3]
Locked by 3, waited [4]
Locked by 3, waited [4]
Locked by 3, waited [4]
Locked by 3, waited [4]
Locked by 3, waited [4]
Locked by 4, waited []
Locked by 4, waited []
Locked by 4, waited []
Locked by 4, waited []
Locked by 4, waited []
上述演示使用的线程数较少,时间差距不大。当增大线程数的时候,可以看到比较明显的结果。
比如,设置成50时,非公平锁耗时:45ms , 公平锁耗时:307ms
性能对比明显。非公平锁性能相对较高。
Why 非公平性能高?
主要有下面两点原因:
1、非公平锁可以减少线程的上下文切换次数,也就意味着运行时间会变快。
通过上面的运行结果可以看出, 公平锁每次都是从等待队列中获取第一个节点让其获得到锁, 而非公平锁则表现为同一个线程多次获取到锁,并没有按照队列顺序获取。这样带来的一个性能的提高点是,因为线程连续获取锁,所以减少了线程的上下文切换,这样耗时便会变小。
2、算是上面第一个原因的补充说明。
公平锁在获取锁的时候,会先检测当前线程是否是等待队列中的第一个节点,如果不是,则无法获取锁,转而切换下个线程尝试获取锁操作。 这样便会带来多次的线程尝试,从而导致上下文切换次数变多,时间也就变长。
具体可以看下二者的代码实现:
获取公平锁的源代码如下:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//这里对当前线程做判断,如果不是等待队列的第一个节点,则无法获取锁,增加了线程切换次数
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
获取非公平锁的源代码如下:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {//无需检测等待队列
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
附两种锁带来的上下文切换次数的统计(线程数5,环境OSX,统计方式mac下的top命令)
备注:
CSW表示(context switch)即上下文切换次数。
命令行:top -o -csw -pid 15989
公平锁:
非公平锁:
如上图所示,非公平锁带来的线程切换数少,当增大程序的线程数及单个线程中获取锁的次数的时候,这两个的数值差距会更大。
这里引申出两个知识点:
1、Mac下如何查看一个进程的上下文切换次数?
使用top命令可以查看。
使用例子:top -o -csw -pid 进程PID
2、Linux下如何查看一个进程的上下文切换次数?
使用pidstat命令查看,
使用例子: pidstat -w -p 15773 1 10
Linux本身是没有这个命令的,需要安装,具体自行百度。
补充一个问题:为什么非公平锁下会出现线程连续获取锁的情况?
使用场景:
根据业务来做判断(说的太虚了,哈哈)。具体暂时还没有遇到使用场景,一般默认为非公平锁。、
如果有使用场景,欢迎邮件给我~~ dutycode@gmail.com
参考资料:
1、Mac如何查看一个进程的上下文切换次数?——> man top
2、Linux如何查看一个进程的上下文切换次数?——> //blog.sina.com.cn/s/blog_9c6f23fb0102x1fg.html
我的博客:www.dutycode.com
我的微信公众号:dutycode_com (中间有个下划线)