当前位置: 首页 > news >正文

ReentrantLock(重入锁)以及公平性

Dedicate to Molly.

简介

ReentrantLock的实现不仅可以替代隐式的synchronized关键字,而且能够提供超过关键字本身的多种功能。
这里提到一个锁获取的公平性问题,如果在绝对时间上,先对锁进行获取的请求一定被先满足,那么这个锁是公平的,反之,是不公平的,也就是说等待时间最长的线程最有机会获取锁,也可以说锁的获取是有序的。ReentrantLock这个锁提供了一个构造函数,能够控制这个锁是否是公平的。
而锁的名字也是说明了这个锁具备了重复进入的可能,也就是说能够让当前线程多次的进行对锁的获取操作,这样的最大次数限制是Integer.MAX_VALUE,约21亿次左右。
事实上公平的锁机制往往没有非公平的效率高,因为公平的获取锁没有考虑到操作系统对线程的调度因素,这样造成JVM对于等待中的线程调度次序和操作系统对线程的调度之间的不匹配。对于锁的快速且重复的获取过程中,连续获取的概率是非常高的,而公平锁会压制这种情况,虽然公平性得以保障,但是响应比却下降了,但是并不是任何场景都是以TPS作为唯一指标的,因为公平锁能够减少“饥饿”发生的概率,等待越久的请求越是能够得到优先满足。

实现分析

在ReentrantLock中,对于公平和非公平的定义是通过对同步器AbstractQueuedSynchronizer的扩展加以实现的,也就是在tryAcquire的实现上做了语义的控制。

非公平的获取语义:

 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;
}

上述逻辑主要包括:

  • 如果当前状态为初始状态,那么尝试设置状态;
  • 如果状态设置成功后就返回;
  • 如果状态被设置,且获取锁的线程又是当前线程的时候,进行状态的自增;
  • 如果未设置成功状态且当前线程不是获取锁的线程,那么返回失败。

公平的获取语义:

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;
}

上述逻辑相比较非公平的获取,仅加入了当前线程(Node)之前是否有前置节点在等待的判断。hasQueuedPredecessors()方法命名有些歧义,其实应该是currentThreadHasQueuedPredecessors()更为妥帖一些,也就是说当前面没有人排在该节点(Node)前面时候队且能够设置成功状态,才能够获取锁。

释放语义:

protected final boolean tryRelease(int releases) {
	int c = getState() - releases;
	if (Thread.currentThread() != getExclusiveOwnerThread())
		throw new IllegalMonitorStateException();
	boolean free = false;
	if (c == 0) {
		free = true;
		setExclusiveOwnerThread(null);
	}
	setState(c);
	return free;
}

上述逻辑主要主要计算了释放状态后的值,如果为0则完全释放,返回true,反之仅是设置状态,返回false。
下面将主要的笔墨放在公平性和非公平性上,首先看一下二者测试的对比:
测试用例如下:

public class ReentrantLockTest {
	private static Lock fairLock = new ReentrantLock(true);
	private static Lock unfairLock = new ReentrantLock();

	@Test
	public void fair() {
		System.out.println("fair version");
		for (int i = 0; i < 5; i++) {
			Thread thread = new Thread(new Job(fairLock));
			thread.setName("" + i);
			thread.start();
		}

		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	@Test
	public void unfair() {
		System.out.println("unfair version");
		for (int i = 0; i < 5; i++) {
			Thread thread = new Thread(new Job(unfairLock));
			thread.setName("" + i);
			thread.start();
		}

		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	private static class Job implements Runnable {
		private 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("Lock by:"
							+ Thread.currentThread().getName());
				} finally {
					lock.unlock();
				}
			}
		}
	}
}

调用非公平的测试方法,返回结果(部分):
unfair version
Lock by:0
Lock by:0
Lock by:2
Lock by:2
Lock by:2
Lock by:2
Lock by:2
Lock by:0
Lock by:0
Lock by:0
Lock by:1
Lock by:1
Lock by:1
调用公平的测试方法,返回结果:
fair version
Lock by:0
Lock by:1
Lock by:0
Lock by:2
Lock by:3
Lock by:4
Lock by:1
Lock by:0
Lock by:2
Lock by:3
Lock by:4
仔细观察返回的结果(其中每个数字代表一个线程),非公平的结果一个线程连续获取锁的情况非常多,而公平的结果连续获取的情况基本没有。那么在一个线程获取了锁的那一刻,究竟锁的公平性会导致锁有什么样的处理逻辑呢?
通过之前的同步器(AbstractQueuedSynchronizer)的介绍,在锁上是存在一个等待队列,sync队列,我们通过复写ReentrantLock的获取当前锁的sync队列,输出在ReentrantLock被获取时刻,当前的sync队列的状态。
修改测试如下:

public class ReentrantLockTest {
	private static Lock fairLock = new ReentrantLock2(true);
	private static Lock unfairLock = new ReentrantLock2();
	@Test
	public void fair() {
		System.out.println("fair version");
		for (int i = 0; i < 5; i++) {
			Thread thread = new Thread(new Job(fairLock)) {
				public String toString() {
					return getName();
				}
			};
			thread.setName("" + i);
			thread.start();
		}
		// sleep 5000ms
	}

	@Test
	public void unfair() {
		System.out.println("unfair version");
		for (int i = 0; i < 5; i++) {
			Thread thread = new Thread(new Job(unfairLock)) {
				public String toString() {
					return getName();
				}
			};
			thread.setName("" + i);
			thread.start();
		}
		// sleep 5000ms
	}

	private static class Job implements Runnable {
		private 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("Lock by:"
							+ Thread.currentThread().getName() + " and "
							+ ((ReentrantLock2) lock).getQueuedThreads()
							+ " waits.");
				} finally {
					lock.unlock();
				}
			}
		}
	}

	private static class ReentrantLock2 extends ReentrantLock {
		// Constructor Override

		private static final long serialVersionUID = 1773716895097002072L;

		public Collection<Thread> getQueuedThreads() {
			return super.getQueuedThreads();
		}
	}
}

上述逻辑主要是通过构造ReentrantLock2用来输出在sync队列中的线程内容,而且每个线程的toString方法被重写,这样当一个线程获取到锁时,sync队列里的内容也就可以得知了,运行结果如下:
调用非公平方法,返回结果:
unfair version
Lock by:0 and [] waits.
Lock by:0 and [] waits.
Lock by:3 and [2, 1] waits.
Lock by:3 and [4, 2, 1] waits.
Lock by:3 and [4, 2, 1] waits.
Lock by:3 and [0, 4, 2, 1] waits.
Lock by:3 and [0, 4, 2, 1] waits.
Lock by:1 and [0, 4, 2] waits.
Lock by:1 and [0, 4, 2] waits.
调用公平方法,返回结果:
fair version
Lock by:0 and [] waits.
Lock by:1 and [0, 4, 3, 2] waits.
Lock by:2 and [1, 0, 4, 3] waits.
Lock by:3 and [2, 1, 0, 4] waits.
Lock by:4 and [3, 2, 1, 0] waits.
Lock by:0 and [4, 3, 2, 1] waits.
Lock by:1 and [0, 4, 3, 2] waits.
Lock by:2 and [1, 0, 4, 3] waits.
可以明显看出,在非公平获取的过程中,“插队”现象非常严重,后续获取锁的线程根本不顾及sync队列中等待的线程,而是能获取就获取。反观公平获取的过程,锁的获取就类似线性化的,每次都由sync队列中等待最长的线程(链表的第一个,sync队列是由尾部结点添加,当前输出的sync队列是逆序输出)获取锁。一个 hasQueuedPredecessors方法能够获得公平性的特性,这点实际上是由AbstractQueuedSynchronizer来完成的,看一下acquire方法:

public final void acquire(int arg) {
	if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
		selfInterrupt();
}

可以看到,如果获取状态和在sync队列中排队是短路的判断,也就是说如果tryAcquire成功,那么是不会进入sync队列的,可以通过下图来深刻的认识公平性和AbstractQueuedSynchronizer的获取过程。
非公平的,或者说默认的获取方式如下图所示:

对于状态的获取,可以快速的通过tryAcquire的成功,也就是黄色的Fast路线,也可以由于tryAcquire的失败,构造节点,进入sync队列中排序后再次获取。因此可以理解为Fast就是一个快速通道,当例子中的线程释放锁之后,快速的通过Fast通道再次获取锁,就算当前sync队列中有排队等待的线程也会被忽略。这种模式,可以保证进入和退出锁的吞吐量,但是sync队列中过早排队的线程会一直处于阻塞状态,造成“饥饿”场景。
而公平性锁,就是在tryAcquire的调用中顾及当前sync队列中的等待节点(废弃了Fast通道),也就是任意请求都需要按照sync队列中既有的顺序进行,先到先得。这样很好的确保了公平性,但是可以从结果中看到,吞吐量就没有非公平的锁高了。

文章转自 并发编程网-ifeve.com

相关文章:

  • 《OOD启思录》—第2章2.2节消息和方法
  • Linux 平台下 Python 脚本编程入门(一)
  • 《网站设计 开发 维护 推广 从入门到精通》—— 1.2 网页美工常用工具
  • 大数据分析提升电子病历临床价值
  • 拆 JakeWharton 系列之 RxAndroid
  • JS---数组(Array)处理函数整理
  • css的元素表现
  • js 正则表达式 判断车牌号
  • Spring7:基于注解的Spring MVC(下篇)
  • js常见知识点2.面向对象相关
  • 20145328《网络对抗》网络欺诈技术防范
  • 09-01 Java final,多态,抽象类,接口
  • 仅作记录,游标,级联删除,获取所有该外键的表名
  • Unity引擎GUI之Image
  • 实体框架(Entity Framework)简介
  • 2017-09-12 前端日报
  • Fabric架构演变之路
  • node和express搭建代理服务器(源码)
  • SQLServer之创建数据库快照
  • vue自定义指令实现v-tap插件
  • 大型网站性能监测、分析与优化常见问题QA
  • 仿天猫超市收藏抛物线动画工具库
  • 复杂数据处理
  • 更好理解的面向对象的Javascript 1 —— 动态类型和多态
  • 聊聊springcloud的EurekaClientAutoConfiguration
  • 数组的操作
  • 协程
  • 《TCP IP 详解卷1:协议》阅读笔记 - 第六章
  • Spark2.4.0源码分析之WorldCount 默认shuffling并行度为200(九) ...
  • # 睡眠3秒_床上这样睡觉的人,睡眠质量多半不好
  • #define、const、typedef的差别
  • #NOIP 2014# day.1 生活大爆炸版 石头剪刀布
  • #NOIP 2014# day.2 T2 寻找道路
  • #stm32整理(一)flash读写
  • (1)Nginx简介和安装教程
  • (6)【Python/机器学习/深度学习】Machine-Learning模型与算法应用—使用Adaboost建模及工作环境下的数据分析整理
  • (编译到47%失败)to be deleted
  • (非本人原创)史记·柴静列传(r4笔记第65天)
  • (每日持续更新)jdk api之FileFilter基础、应用、实战
  • (学习日记)2024.01.19
  • (一)认识微服务
  • (轉)JSON.stringify 语法实例讲解
  • (自适应手机端)响应式新闻博客知识类pbootcms网站模板 自媒体运营博客网站源码下载
  • **PyTorch月学习计划 - 第一周;第6-7天: 自动梯度(Autograd)**
  • .form文件_一篇文章学会文件上传
  • .NET Core WebAPI中封装Swagger配置
  • .project文件
  • .py文件应该怎样打开?
  • @取消转义
  • [ C++ ] STL---string类的模拟实现
  • [ linux ] linux 命令英文全称及解释
  • [ Linux 长征路第五篇 ] make/Makefile Linux项目自动化创建工具
  • [Angular] 笔记 8:list/detail 页面以及@Input
  • [Big Data - Kafka] kafka学习笔记:知识点整理
  • [C++ 从入门到精通] 12.重载运算符、赋值运算符重载、析构函数