锁的公平和非公平

losetowin 发布于:2016-11-20 20:39 分类:Java  有 6432 人浏览,获得评论 0 条 标签: 公平锁 非公平锁 多线程 并发 

本文地址:http://www.dutycode.com/suo_gongping_feigongping.html
除非注明,文章均为 www.dutycode.com 原创,欢迎转载!转载请注明本文地址,谢谢。
简单介绍

在锁的获取上,存在一个公平性和不公平性的问题,所谓的公平是指,在一段时间内,先对锁发起获取请求的一定被先满足。或者可以理解成期望获取锁的线程是一个先进先出的队列,等待时间最久的线程最优先获取到锁。而非公平是指,获取锁的顺序并不是有序的,可以随时插队。就好比都在排队检票,突然来了一个人说他的火车马上就要开车了,想插在你前面检票进站,这时候你就有可能仁慈了,把位置让他插入了。 这时候,这个人就优先获取到了锁。 但这个并不公平。

性能测试

两种锁的获取方式性能不一样,一般情况下公平的锁机制比非公平的效率低,因为公平的锁机制没有考虑到操作系统对线程的调度,会造成线程的上下文切换次数增加。(还有一种比较专业的说法:因为公平的获取锁没有考虑到操作系统对线程的调度因素,这样造成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<String> getQuenedThreads() {
			List<String> threadList = new ArrayList<String>();
			Collection<Thread> colls = super.getQueuedThreads();
			Iterator<Thread> it = colls.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 
公平锁:(点击图片查看原图)
QQ20161120-2.png
非公平锁:(点击图片查看原图)
QQ20161120-6.png
如上图所示,非公平锁带来的线程切换数少,当增大程序的线程数及单个线程中获取锁的次数的时候,这两个的数值差距会更大。

这里引申出两个知识点:
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如何查看一个进程的上下文切换次数?——> http://blog.sina.com.cn/s/blog_9c6f23fb0102x1fg.html


版权所有:《攀爬蜗牛》 => 《锁的公平和非公平
本文地址:https://www.dutycode.com/suo_gongping_feigongping.html
除非注明,文章均为 《攀爬蜗牛》 原创,欢迎转载!转载请注明本文地址,谢谢。