Ala*_*ent 27 java locking compare-and-swap
我正在实现请求实例的FIFO队列(预先分配的请求对象以获得速度),并在add方法上使用"synchronized"关键字开始.该方法非常短(检查固定大小缓冲区中的空间,然后向数组添加值).使用visualVM看起来线程比我更喜欢阻塞("监视器"准确).因此,我将代码转换为使用AtomicInteger值,例如跟踪当前大小,然后在while循环中使用compareAndSet()(因为AtomicInteger在内部为incrementAndGet()等方法执行).代码现在看起来要长一点.
我想知道的是使用synchronized和更短代码的性能开销与没有synchronized关键字的更长代码相比(因此永远不应该阻塞锁).
这是使用synchronized关键字的旧get方法:
public synchronized Request get()
{
if (head == tail)
{
return null;
}
Request r = requests[head];
head = (head + 1) % requests.length;
return r;
}
Run Code Online (Sandbox Code Playgroud)
这是没有synchronized关键字的新get方法:
public Request get()
{
while (true)
{
int current = size.get();
if (current <= 0)
{
return null;
}
if (size.compareAndSet(current, current - 1))
{
break;
}
}
while (true)
{
int current = head.get();
int nextHead = (current + 1) % requests.length;
if (head.compareAndSet(current, nextHead))
{
return requests[current];
}
}
}
Run Code Online (Sandbox Code Playgroud)
我的猜测是synchronized关键字更糟,因为锁定的风险(可能导致线程上下文切换等),即使代码更短.
谢谢!
Pét*_*rök 33
我的猜测是synchronized关键字更糟,因为锁定的风险(可能导致线程上下文切换等)
是的,在一般情况下你是对的.Java Concurrency in Practice在第15.3.2节中讨论了这一点:
[...]在高争用级别锁定往往优于原子变量,但在更现实的争用级别,原子变量优于锁定.这是因为锁通过挂起线程来对争用作出反应,从而减少共享内存总线上的CPU使用率和同步流量.(这类似于生产者 - 消费者设计中的阻塞生产者如何减少消费者的负担,从而让他们赶上来.)另一方面,通过原子变量,争用管理被推回到调用类.像大多数基于CAS的算法一样,
AtomicPseudoRandom
通过立即再次尝试来对争用做出反应,这通常是正确的方法,但在高争用环境中只会产生更多争用.在我们谴责
AtomicPseudoRandom
写作不好或原子变量与锁定相比作为一个糟糕的选择之前,我们应该意识到图15.1中的争用程度是不切实际的高:没有真正的程序只会争夺锁或原子变量.在实践中,原子倾向于比锁更好地扩展,因为原子更有效地处理典型的争用水平.在不同的争用水平下,锁和原子之间的性能逆转说明了各自的优缺点.由于低到中等的争用,原子提供了更好的可扩展性; 在高争用的情况下,锁可以提供更好的争用避免.(基于CAS的算法在单CPU系统上也优于基于锁定的算法,因为CAS总是在单CPU系统上成功,除非在读 - 修改 - 写操作过程中线程被抢占的不太可能的情况. )
(在文中提到的数字上,图15.1显示,当争用率很高时,AtomicInteger和ReentrantLock的性能或多或少相等,而图15.2显示,在中等争用下,前者的表现优于后者2-3倍.)
正如其他人所指出的那样,非阻塞算法虽然可能更快,但更复杂,因此更难以正确实现.JCiA第15.4节的提示:
很好的非阻塞算法对于许多常见的数据结构是已知的,包括堆栈,队列,优先级队列和哈希表,尽管设计新的算法最好留给专家.
非阻塞算法比基于锁的等效算法复杂得多.创建非阻塞算法的关键是弄清楚如何在保持数据一致性的同时将原子更改的范围限制为单个变量.在诸如队列之类的链接集合类中,您有时可以将状态转换表示为对单个链接的更改,并使用a
AtomicReference
来表示必须以原子方式更新的每个链接.