当前位置: 时代头条 > 正文

锁的公平和非公平

简单介绍:

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


性能测试:

两种锁的获取方式性能不一样,一般情况下公平的锁机制比非公平的效率低,因为公平的锁机制没有考虑到操作系统对线程的调度,会造成线程的上下文切换次数增加。(还有一种比较专业的说法:因为公平的获取锁没有考虑到操作系统对线程的调度因素,这样造成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 getQuenedThreads() {

List threadList = new ArrayList();

Collection colls = super.getQueuedThreads();

Iterator 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

公平锁:

非公平锁:

如上图所示,非公平锁带来的线程切换数少,当增大程序的线程数及单个线程中获取锁的次数的时候,这两个的数值差距会更大。

这里引申出两个知识点:

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 (中间有个下划线)

最新文章

取消
扫码支持 支付码