我正在实现请求实例的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;
}
以下是没有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];
}
}
}
我的猜测是synchronized关键字更糟,因为锁定的风险(可能导致线程上下文切换等),即使代码较短。
谢谢!
答案 0 :(得分: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节的提示:
许多常见的数据结构都有很好的非阻塞算法,包括堆栈,队列,优先级队列和哈希表,尽管设计新的算法最好留给专家。
非阻塞算法比基于锁定的等效算法复杂得多。创建非阻塞算法的关键是弄清楚如何在保持数据一致性的同时将原子更改的范围限制为单个变量。在链接集合类(如队列)中,您有时可以将状态转换表示为对单个链接的更改,并使用
AtomicReference
表示必须以原子方式更新的每个链接。
答案 1 :(得分:4)
我想知道在真正暂停线程之前jvm是否已经进行了一些旋转。它预计,与你的一样,写得很好的关键部分几乎可以立即完成。因此,在放弃和暂停线程之前,应该乐观地等待,我不知道,有几十个循环。如果是这种情况,它的行为应与第二个版本相同。
分析器显示的内容可能与全速运行的jvm中的内容有很大不同,具有各种疯狂的优化。最好是在没有分析器的情况下测量和比较吞吐量。
答案 2 :(得分:1)
在进行这种同步优化之前,你真的需要一个分析器来告诉你它是绝对必要的。
是的,在某些条件下同步可能比原子操作慢,但比较原始方法和替换方法。前者非常清晰,易于维护,后者非常复杂。因此,可能存在非常微妙的并发错误,在初始测试期间您将找不到。我已经看到一个问题,size
和head
实际上可能不同步,因为虽然这些操作中的每一个都是原子的,但组合不是,有时这可能会导致状态不一致。 / p>
所以,我的建议:
答案 3 :(得分:0)
这是繁忙等待锁的代码。
public class BusyWaitLock
{
private static final boolean LOCK_VALUE = true;
private static final boolean UNLOCK_VALUE = false;
private final static Logger log = LoggerFactory.getLogger(BusyWaitLock.class);
/**
* @author Rod Moten
*
*/
public class BusyWaitLockException extends RuntimeException
{
/**
*
*/
private static final long serialVersionUID = 1L;
/**
* @param message
*/
public BusyWaitLockException(String message)
{
super(message);
}
}
private AtomicBoolean lock = new AtomicBoolean(UNLOCK_VALUE);
private final long maximumWaitTime ;
/**
* Create a busy wait lock with that uses the default wait time of two minutes.
*/
public BusyWaitLock()
{
this(1000 * 60 * 2); // default is two minutes)
}
/**
* Create a busy wait lock with that uses the given value as the maximum wait time.
* @param maximumWaitTime - a positive value that represents the maximum number of milliseconds that a thread will busy wait.
*/
public BusyWaitLock(long maximumWaitTime)
{
if (maximumWaitTime < 1)
throw new IllegalArgumentException (" Max wait time of " + maximumWaitTime + " is too low. It must be at least 1 millisecond.");
this.maximumWaitTime = maximumWaitTime;
}
/**
*
*/
public void lock ()
{
long startTime = System.currentTimeMillis();
long lastLogTime = startTime;
int logMessageCount = 0;
while (lock.compareAndSet(UNLOCK_VALUE, LOCK_VALUE)) {
long waitTime = System.currentTimeMillis() - startTime;
if (waitTime - lastLogTime > 5000) {
log.debug("Waiting for lock. Log message # {}", logMessageCount++);
lastLogTime = waitTime;
}
if (waitTime > maximumWaitTime) {
log.warn("Wait time of {} exceed maximum wait time of {}", waitTime, maximumWaitTime);
throw new BusyWaitLockException ("Exceeded maximum wait time of " + maximumWaitTime + " ms.");
}
}
}
public void unlock ()
{
lock.set(UNLOCK_VALUE);
}
}