为了了解锁的工作方式,实现自定义锁是一个好方法。 这篇文章将展示如何在Java上实现Filter和Bakery锁(自旋锁),并将它们的性能与Java的ReentrantLock进行比较。 过滤器锁和面包房锁满足互斥并且也是无饥饿算法,面包房锁是先到先服务的锁[1]。
为了进行性能测试,使用不同的锁类型,不同的线程数和不同的次数将计数器值递增到10000000。 测试系统配置为:Intel Core I7(具有8个核心,其中4个是真实的),Ubuntu 14.04 LTS和Java 1.7.0_60。
过滤器锁具有n-1个级别,可以视为“候诊室”。 获取锁之前,必须有一个线程穿过此等候室。 级别[2]有两个重要属性:
- 至少一个尝试进入级别l的线程成功。
- 如果有多个线程试图进入级别l ,则至少一个线程被阻止(即继续在该级别上等待)。
过滤器锁定的实现如下:
/**
* @author Furkan KAMACI
*/
public class Filter extends AbstractDummyLock implements Lock {
/* Due to Java Memory Model, int[] not used for level and victim variables.
Java programming language does not guarantee linearizability, or even sequential consistency,
when reading or writing fields of shared objects
[The Art of Multiprocessor Programming. Maurice Herlihy, Nir Shavit, 2008, pp.61.]
*/
private AtomicInteger[] level;
private AtomicInteger[] victim;
private int n;
/**
* Constructor for Filter lock
*
* @param n thread count
*/
public Filter(int n) {
this.n = n;
level = new AtomicInteger[n];
victim = new AtomicInteger[n];
for (int i = 0; i < n; i++) {
level[i] = new AtomicInteger();
victim[i] = new AtomicInteger();
}
}
/**
* Acquires the lock.
*/
@Override
public void lock() {
int me = ConcurrencyUtils.getCurrentThreadId();
for (int i = 1; i < n; i++) {
level[me].set(i);
victim[i].set(me);
for (int k = 0; k < n; k++) {
while ((k != me) && (level[k].get() >= i && victim[i].get() == me)) {
//spin wait
}
}
}
}
/**
* Releases the lock.
*/
@Override
public void unlock() {
int me = ConcurrencyUtils.getCurrentThreadId();
level[me].set(0);
}
}
面包店锁定算法通过使用面包店中常见的数字分配机的分布式版本来维护先到先得的属性:每个线程在门口取一个数字,然后等待,直到没有尝试使用更早编号的线程为止输入[3]。
面包店锁的实现如下:
/**
* @author Furkan KAMACI
*/
public class Bakery extends AbstractDummyLock implements Lock {
/* Due to Java Memory Model, int[] not used for level and victim variables.
Java programming language does not guarantee linearizability, or even sequential consistency,
when reading or writing fields of shared objects
[The Art of Multiprocessor Programming. Maurice Herlihy, Nir Shavit, 2008, pp.61.]
*/
private AtomicBoolean[] flag;
private AtomicInteger[] label;
private int n;
/**
* Constructor for Bakery lock
*
* @param n thread count
*/
public Bakery(int n) {
this.n = n;
flag = new AtomicBoolean[n];
label = new AtomicInteger[n];
for (int i = 0; i < n; i++) {
flag[i] = new AtomicBoolean();
label[i] = new AtomicInteger();
}
}
/**
* Acquires the lock.
*/
@Override
public void lock() {
int i = ConcurrencyUtils.getCurrentThreadId();
flag[i].set(true);
label[i].set(findMaximumElement(label) + 1);
for (int k = 0; k < n; k++) {
while ((k != i) && flag[k].get() && ((label[k].get() < label[i].get()) || ((label[k].get() == label[i].get()) && k < i))) {
//spin wait
}
}
}
/**
* Releases the lock.
*/
@Override
public void unlock() {
flag[ConcurrencyUtils.getCurrentThreadId()].set(false);
}
/**
* Finds maximum element within and {@link java.util.concurrent.atomic.AtomicInteger} array
*
* @param elementArray element array
* @return maximum element
*/
private int findMaximumElement(AtomicInteger[] elementArray) {
int maxValue = Integer.MIN_VALUE;
for (AtomicInteger element : elementArray) {
if (element.get() > maxValue) {
maxValue = element.get();
}
}
return maxValue;
}
}
对于此类算法,应提供或使用从0或1开始并以一个增量递增的线程id系统。 线程的名称为此目的进行了适当设置。 还应该考虑:Java编程语言在读取或写入共享对象的字段时不能保证线性化甚至顺序一致性[4]。 因此,过滤器锁的级别和受害变量,面包店锁的标志和标签变量定义为原子变量。 一方面,想要测试Java内存模型效果的人可以将该变量更改为int []和boolean [],并使用两个以上的线程运行算法。 然后,可以看到即使线程处于活动状态,该算法也将针对Filter或Bakery挂起。
为了测试算法性能,实现了一个自定义计数器类,该类具有getAndIncrement方法,如下所示:
/**
* gets and increments value up to a maximum number
*
* @return value before increment if it didn't exceed a defined maximum number. Otherwise returns maximum number.
*/
public long getAndIncrement() {
long temp;
lock.lock();
try {
if (value >= maxNumber) {
return value;
}
temp = value;
value = temp + 1;
} finally {
lock.unlock();
}
return temp;
}
公平测试多个应用程序配置存在最大的障碍。 考虑的是:有很多工作(将变量递增到所需的数量),并且在线程数量不同的情况下,完成它的速度有多快。 因此,为了进行比较,应该有一个“工作”平等。 此方法还使用该代码段测试不必要的工作负载:
if (value >= maxNumber) {
return value;
}
比较多个线程时,一种计算线程的单位工作性能的方法(即,不设置最大障碍,在循环中迭代到最大数量,然后将最后一个值除以线程数量)。
此配置用于性能比较:
线程数 | 1,2,3,4,5,6,7,8 |
重试计数 | 20 |
最大人数 | 10000000 |
这是包含标准误差的结果图表:
首先,当您在Java中多次运行代码块时,会对代码进行内部优化。 当算法多次运行并将第一输出与第二输出进行比较时,可以看到此优化的效果。 因此,第一次经过的时间通常应大于第二行。 例如:
currentTry = 0, threadCount = 1, maxNumber = 10000000, lockType = FILTER, elapsedTime = 500 (ms)
currentTry = 1, threadCount = 1, maxNumber = 10000000, lockType = FILTER, elapsedTime = 433 (ms)
结论
从图表中可以看出,面包房锁比过滤器锁快,标准误差低。 原因是筛选器锁定的锁定方法。 在Bakery Lock中,作为一种公平的方法,线程是一个一个地运行的,但是在Filter Lock中,它们是相互计算的。 与其他Java相比,Java的ReentrantLock具有最佳的性能。
另一方面,Filter Lock线性地变差,但是Bakery和ReentrantLock却不是(当线程运行更多的线程时,Filter Lock可能具有线性图形)。 更多的线程数并不意味着更少的经过时间。 由于创建和锁定/解锁线程,因此2个线程可能比1个线程差。 当线程数开始增加时,Bakery和ReentrantLock的经过时间会变得更好。 但是,当线程数持续增加时,它就会变得更糟。 原因是运行算法的测试计算机的真实核心编号。
- 可以从此处下载用于在Java中实现过滤器和面包店锁的源代码: https : //github.com/kamaci/filbak
- 多处理器编程的艺术。 莫里斯·赫里希(Maurice Herlihy),《尼尔·沙维特》(Nir Shavit),2008年,第31.-33页。
- 多处理器编程的艺术。 莫里斯·赫里希(Maurice Herlihy),《尼尔·沙维特》(Nir Shavit),2008年,第28页。
- 多处理器编程的艺术。 莫里斯·赫利希(Maurice Herlihy),《尼尔·沙维特》(Nir Shavit),2008年,第31页。
- 多处理器编程的艺术。 莫里斯·赫利希(Maurice Herlihy),《尼尔·沙维特》(Nir Shavit),2008年,第61页。
翻译自: https://www.javacodegeeks.com/2015/05/implementing-filter-and-bakery-locks-in-java.html