?/span>入锁Q?span class="hilite1" style="background-color: #ffff00; ">ReentrantLockQ是一U递归无阻塞的同步机制。以前一直认为它是synchronized的简单替代,而且实现机制也不相差太远。不q最q实践过E中发现它们之间q是有着天壤之别?/p>
以下?a target="_blank" style="color: #006699; text-decoration: underline; ">官方说明Q一个可重入的互斥锁?LockQ它h与?synchronized Ҏ(gu)和语句所讉K的隐式监视器锁定相同的一些基本行为和语义Q但功能更强大?span class="hilite1" style="background-color: #ffff00; ">ReentrantLock 由最q成功获得锁定,q且q没有释放该锁定的线E所拥有。当锁定没有被另一个线E所拥有Ӟ调用 lock 的线E将成功获取该锁定ƈq回。如果当前线E已l拥有该锁定Q此Ҏ(gu)立卌回。可以?isHeldByCurrentThread() ?getHoldCount() Ҏ(gu)来检查此情况是否发生?/p>
它提供了(jin)lock()Ҏ(gu)Q?br /> 如果该锁定没有被另一个线E保持,则获取该锁定q立卌回,锁定的保持计数讄?1?br /> 如果当前U程已经保持该锁定,则将保持计数?1Qƈ且该Ҏ(gu)立即q回?br /> 如果该锁定被另一个线E保持,则出于线E调度的目的Q禁用当前线E,q且在获得锁定之前,该线E将一直处于休眠状态,此时锁定保持计数被设|ؓ(f) 1?/p>
最q在研究Java concurrent中关于Q务调度的实现ӞM(jin)延迟队列DelayQueue的一些代码,比如take()。该Ҏ(gu)的主要功能是从优先队列(PriorityQueueQ取Z个最应该执行的Q务(最优|(j)Q如果该d的预订执行时间未刎ͼ则需要waitq段旉差。反之,如果旉C(jin)Q则q回该Q务。而offer()Ҏ(gu)是将一个Q务添加到该队列中?/p>
后来产生?jin)一个疑问:(x)如果最应该执行的Q务是一个小时后执行的,而此旉要提交一?0U后执行的Q务,?x)出C么状况?q是先看看take()的源代码Q?/p>
而以下是offer()的源代码:
如代码所C,take()和offer()都是lock?jin)重入锁。如果按照synchronized的思维Q用诸如synchronized(obj)的方法)(j)Q这两个Ҏ(gu)是互斥的。回到刚才的疑问Qtake()Ҏ(gu)需要等?个小时才能返回,而offer()需要马上提交一?0U后q行的Q务,?x)不会(x)一直等待take()q回后才能提交呢Q答案是否定的,通过~写验证代码也说明了(jin)q一炏V这让我寚w入锁有了(jin)更大的兴,它确实是一个无d的锁?/p>
下面的代码也许能说明问题Q运行了(jin)4个线E,每一ơ运行前打印l(f)ock的当前状态。运行后都要{待5U钟?/p>
q是它的输出Q?br />
Pre ReentrantLock@a59698[Unlocked] 每一个线E的锁状态都?#8220;Unlocked”,所以都可以q行。但在把con.awaitҎ(gu)Thread.sleep(5000)Ӟ输出变成了(jin)Q?br />
Pre ReentrantLock@a59698[Unlocked] 以上的对比说明线E在{待?con.await)Q已l不在拥有(keepQ该锁了(jin)Q所以其他线E就可以获得重入锁了(jin)?br />
有必要会(x)q头再看看Java官方的解释:(x)“如果该锁定被另一个线E保持,则出于线E调度的目的Q禁用当前线E,q且在获得锁定之前,该线E将一直处于休眠状?#8221;。我对这里的“保持”的理解是指非wait状态外的所有状态,比如U程Sleep、for循环{一切有CPU参与的活动。一旦线E进入wait状态后Q它?yu)׃再keepq个锁了(jin)Q其他线E就可以获得该锁Q当该线E被唤醒Q触发信h者timeoutQ后Q就接着执行Q会(x)重新“保持”锁,当然前提依然是其他线E已l不?#8220;保持”?jin)该重入锁?/p>
ȝ一句话Q对于重入锁而言Q?lock"?keep"是两个不同的概念。lock?jin)锁Q不一定keep锁,但keep?jin)锁一定已llock?jin)锁?/p>
Pre ReentrantLock@a59698[Unlocked]
Pre ReentrantLock@a59698[Unlocked]
Pre ReentrantLock@a59698[Unlocked]
Post ReentrantLock@a59698[Locked by thread pool-1-thread-1]
Post ReentrantLock@a59698[Locked by thread pool-1-thread-2]
Post ReentrantLock@a59698[Locked by thread pool-1-thread-3]
Post ReentrantLock@a59698[Locked by thread pool-1-thread-4]
Pre ReentrantLock@a59698[Locked by thread pool-1-thread-1]
Pre ReentrantLock@a59698[Locked by thread pool-1-thread-1]
Pre ReentrantLock@a59698[Locked by thread pool-1-thread-1]
Post ReentrantLock@a59698[Locked by thread pool-1-thread-1]
Post ReentrantLock@a59698[Locked by thread pool-1-thread-2]
Post ReentrantLock@a59698[Locked by thread pool-1-thread-3]
Post ReentrantLock@a59698[Locked by thread pool-1-thread-4]
十五q前Q多处理器系l是高度专用pȝQ要p数十万美元(大多数具有两个到四个处理器)(j)。现在,多处理器pȝ很便宜,而且数量很多Q几乎每个主要微处理器都内置?jin)多处理支持Q其中许多系l支持数十个或数百个处理器?/p>
要用多处理器系l的功能Q通常需要用多U程构造应用程序。但是正如Q何编写ƈ发应用程序的人可以告诉你的那P要获得好的硬件利用率Q只是简单地在多个线E中分割工作是不够的Q还必须保U程实大部分时间都在工作,而不是在{待更多的工作,或等待锁定共享数据结构?/p>
如果U程之间 ?/em>需要协调,那么几乎没有d可以真正地ƈ行。以U程池ؓ(f)例,其中执行的Q务通常怺独立。如果线E池利用公共工作队列Q则从工作队列中删除元素或向工作队列d元素的过E必LU程安全的,q且q意味着要协调对头、尾或节炚w链接指针所q行的访问。正是这U协调导致了(jin)所有问题?/p>
?Java 语言中,协调对共享字D늚讉K的传l方法是使用同步Q确保完成对׃n字段的所有访问,同时h适当的锁定。通过同步Q可以确定(假设cȝ写正)(j)h保护一l给定变量的锁定的所有线E都拥有对q些变量的独占访问权Qƈ且以后其他线E获得该锁定Ӟ可以看到对q些变量q行的更攏V弊端是如果锁定竞争太厉宻IU程常常在其他线E具有锁定时要求获得该锁定)(j)Q会(x)损害吞吐量,因ؓ(f)竞争的同步非常昂c(din)(Public Service AnnouncementQ对于现?JVM 而言Q无竞争的同步现在非怾宜?/p>
Z锁定的算法的另一个问题是Q如果gq具有锁定的U程Q因为页面错误、计划gq或其他意料之外的gq)(j)Q则 没有要求获得该锁定的U程可以l箋q行?/p>
q可以用可变变量来以比同步更低的成本存储共享变量,但它们有局限性。虽然可以保证其他变量可以立即看到对可变变量的写入,但无法呈现原子操作的?修改-写顺序,q意味着Q比如说Q可变变量无法用来可靠地实现互斥Q互斥锁定)(j)或计数器?/p>
假如开发线E安全的计数器类Q那么这暴?
许多q发法中都昄?jin)原子的?修改-写组合。清?2 中的代码实现?jin)简单的互斥Q?
清单 1 中的计数器类可以可靠地工作,在竞争很或没有竞争旉可以很好地执行。然而,在竞争激烈时Q这大大损x能Q因?JVM 用了(jin)更多的时间来调度U程Q管理竞争和{待U程队列Q而实际工作(如增加计数器Q的旉却很。?zhn)可以回?上月专栏中的图,该图昄?jin)一旦多个线E用同步竞争一个内|监视器Q吞吐量如何大q度下降。虽然该专栏说明?jin)新? 使用锁定Q如果一个线E试图获取其他线E已l具有的锁定Q那么该U程被dQ直到该锁定可用。此Ҏ(gu)h一些明昄~点Q其中包括当U程被阻塞来{待锁定Ӟ它无法进行其他Q何操作。如果阻塞的U程是高优先U的dQ那么该Ҏ(gu)可能造成非常不好的结果(UCؓ(f) 优先U倒置的危险)(j)?/span> 使用锁定q有一些其他危险,如死锁(当以不一致的序获得多个锁定时会(x)发生死锁Q。甚x有这U危险,锁定也仅是相对的_粒度协调机Ӟ同样非常适合理单操作,如增加计数器或更C斥拥有者。如果有更细_度的机制来可靠理对单独变量的q发更新Q则?x)更好一些;在大多数C处理器都有这U机制?/span> 如前所qͼ大多数现代处理器都包含对多处理的支持。当然这U支持包括多处理器可以共享外部设备和d存,同时它通常q包括对指o(h)pȝ的增加来支持多处理的Ҏ(gu)要求。特别是Q几乎每个现代处理器都有通过可以(g)或L其他处理器的q发讉K的方式来更新׃n变量的指令?/span> 支持q发的第一个处理器提供原子的测试ƈ讄操作Q通常在单位上q行q项操作。现在的处理器(包括 Intel ?Sparc 处理器)(j)使用的最通用的方法是实现名ؓ(f) 比较q{?/span>?CAS 的原语。(?Intel 处理器中Q比较ƈ交换通过指o(h)?cmpxchg pd实现。PowerPC 处理器有一对名?#8220;加蝲q保?#8221;?#8220;条g存储”的指令,它们实现相同的目圎ͼMIPS ?PowerPC 处理器相|除了(jin)W一个指令称?#8220;加蝲链接”。)(j) CAS 操作包含三个操作?—?内存位置QVQ、预期原|AQ和新?B)。如果内存位|的g预期原值相匚wQ那么处理器?x)自动将该位|值更Cؓ(f)新倹{否则,处理器不做Q何操作。无论哪U情况,它都?x)?CAS 指o(h)之前q回该位|的倹{(?CAS 的一些特D情况下仅q回 CAS 是否成功Q而不提取当前倹{)(j)CAS 有效地说明了(jin)“我认Z|?V 应该包含?AQ如果包含该|则将 B 攑ֈq个位置Q否则,不要更改该位|,只告诉我q个位置现在的值即可?#8221; 通常?CAS 用于同步的方式是从地址 V d?AQ执行多步计来获得新?BQ然后?CAS ?V 的g A 改ؓ(f) B。如?V 处的值尚未同时更改,?CAS 操作成功?/span> cM?CAS 的指令允许算法执行读-修改-写操作,而无需x其他线E同时修改变量,因ؓ(f)如果其他U程修改变量Q那?CAS ?x)检它Qƈp|Q,法可以对该操作重新计算。清?3 说明?CAS 操作的行为(而不是性能特征Q,但是 CAS 的h(hun)值是它可以在g中实玎ͼq且是极轻量U的Q在大多数处理器中)(j)Q?/span>
Z CAS 的ƈ发算法称?无锁?/em>法Q因为线E不必再{待锁定Q有时称Z斥或关键部分Q这取决于线E^台的术语Q。无?CAS 操作成功q是p|Q在M一U情况中Q它都在可预知的旉内完成。如?CAS p|Q调用者可以重?CAS 操作或采取其他适合的操作。清?4 昄?jin)重新编写的计数器类来?CAS 替代锁定Q?/p>
如果每个U程在其他线EQ意gq(或甚臛_败)(j)旉持l进行操作,可以说该算法是 无等?/em>的。与此Ş成对比的是, 无锁?/em>法要求?#160;某个U程L执行操作。(无等待的另一U定义是保证每个U程在其有限的步骤中正确计算自己的操作,而不其他线E的操作、计时、交叉或速度。这一限制可以是系l中U程数的函数Q例如,如果?10 个线E,每个U程都执行一?#160; 再过ȝ 15 q里Qh们已l对无等待且无锁定算法(也称?#160;无阻塞算?/em>Q进行了(jin)大量研究Q许多h通用数据l构已经发现?jin)无d法。无d法被广泛用于操作系l和 JVM U别Q进行诸如线E和q程调度{Q务。虽然它们的实现比较复杂Q但相对于基于锁定的备选算法,它们有许多优点:(x)可以避免优先U倒置和死锁等危险Q竞争比较便宜,协调发生在更l的_度U别Q允许更高程度的q行机制{等?/p>
?JDK 5.0 之前Q如果不使用本机代码Q就不能?Java 语言~写无等待、无锁定的算法。在 原子变量cd以认为是 虽然原子变量c表面看h与清?1 中的 调整h竞争的ƈ发应用程序的可~性的通用技术是降低使用的锁定对象的_度Q希望更多的锁定h从竞争变Z竞争。从锁定转换为原子变量可以获得相同的l果Q通过切换为更l粒度的协调机制Q竞争的操作更,从而提高了(jin)吞吐量?/p>
无论是直接的q是间接的,几乎 如果没有 JDK 5.0 中的 JVM 改进Q将无法构造这些类Q这些改q暴露了(jin)Q向cdQ而不是用L(fng)Q接口来讉KgU的同步原语。然后,java.util.concurrent 中的原子变量cd其他cd用户cd开q些功能?/p>
上月Q我介绍?#160; 清单 5 昄?jin)用同步?PRNG 实现和?CAS 备选实现。注意,要在循环中执?CASQ因为它可能?x)失败一ơ或多次才能获得成功Q?CAS 的代码Lq样?/p>
下面?1 和图 2 中的图与上月那些囄|只是为基于原子的Ҏ(gu)多添加了(jin)一行。这些图昄?jin)?8-way Ultrasparc3 和单处理?Pentium 4 上用不同数量线E的随机发生的吞吐量Q以每秒转数为单位)(j)。测试中的线E数不是真实的;q些U程所表现的竞争比通常多得多,所以它们以比实际程序中低得多的U程数显CZ(jin) 大多数用户都不太可能使用原子变量自己开发无d法 ?他们更可能?#160; 开发h员可以直接将原子变量用作׃n计数器、序L(fng)成器和其他独立共享变量的高性能替代Q否则必通过同步保护q些变量?/p>
JDK 5.0 是开发高性能q发cȝ巨大q步。通过内部公开新的低协调原语Q和提供一l公共原子变量类Q现在用 Java 语言开发无{待、无锁定法首次变ؓ(f)可行。然后, get()
?increment()
?decrement()
操作。清?1 昄?jin)如何用锁定(同步Q实现该cȝ例子。注意所有方法,甚至需要同?get()
QɾcL为线E安全的c,从而确保没有Q何更C息丢失,所有线E都看到计数器的最新倹{?/p>
清单 1. 同步的计数器c?/strong>
private int value;
public synchronized int getValue() { return value; }
public synchronized int increment() { return ++value; }
public synchronized int decrement() { return --value; }
}increment()
?decrement()
操作是原子的?修改-写操作,Z(jin)安全实现计数器,必须使用当前|qؓ(f)其添加一个|或写出新|所有这些均视ؓ(f)一Ҏ(gu)作,其他U程不能打断它。否则,如果两个U程试图同时执行增加Q操作的不幸交叉导致计数器只被实现?jin)一ơ,而不是被实现两次。(注意Q通过使值实例变量成为可变变量ƈ不能可靠地完成这Ҏ(gu)作。)(j)acquire()
Ҏ(gu)也是原子的读-修改-写操作。要获得互斥Q必ȝ保没有其他hh该互斥( curOwner = Thread.currentThread()
Q,然后记录(zhn)拥有该互斥的事实( curOwner = Thread.currentThread()
Q,所有这些其他U程不可能在中间出现以及(qing)修改 curOwner field
?/p>
private Thread curOwner = null;
public synchronized void acquire() throws InterruptedException {
if (Thread.interrupted()) throw new InterruptedException();
while (curOwner != null)
wait();
curOwner = Thread.currentThread();
}
public synchronized void release() {
if (curOwner == Thread.currentThread()) {
curOwner = null;
notify();
} else
throw new IllegalStateException("not owner of mutex");
}
}ReentrantLock
cd何可以更可~地替代同步Q但是对于一些问题,q有更好的解x法?/span>
private int value;
public synchronized int getValue() { return value; }
public synchronized int compareAndSwap(int expectedValue, int newValue) {
if (value == expectedValue)
value = newValue;
return value;
}
}
private SimulatedCAS value;
public int getValue() {
return value.getValue();
}
public int increment() {
int oldValue = value.getValue();
while (value.compareAndSwap(oldValue, oldValue + 1) != oldValue)
oldValue = value.getValue();
return oldValue + 1;
}
}CasCounter.increment()
操作Q最坏的情况下,每个U程必重试最多九(ji)ơ,才能完成增加。)(j)java.util.concurrent.atomic
包中d原子变量cM后,q种情况才发生了(jin)改变。所有原子变量类都公开比较q设|原语(与比较ƈ交换cMQ,q些原语都是使用q_上可用的最快本机结构(比较q交换、加载链?条g存储Q最坏的情况下是旋{锁)(j)来实现的?#160;java.util.concurrent.atomic
包中提供?jin)原子变量?9 U风| AtomicInteger
Q?#160;AtomicLong
Q?#160;AtomicReference
Q?#160;AtomicBoolean
Q原子整型;长型Q引用;?qing)原子标记引用和戌引用cȝ数组形式Q其原子地更C对|(j)?/p>
volatile
变量的泛化,它扩展了(jin)可变变量的概念,来支持原子条件的比较q设|更新。读取和写入原子变量与读取和写入对可变变量的讉Kh相同的存取语义?/p>
SynchronizedCounter
例子一P但相g是表面的。在表面之下Q原子变量的操作?x)变为^台提供的用于q发讉K的硬件原语,比如比较q交换?/p>
ABA 问题
因ؓ(f)在更?V 之前QCAS 主要询问“V 的值是否仍?A”Q所以在W一ơ读?V 以及(qing)?V 执行 CAS 操作之前Q如果将g A 改ؓ(f) BQ然后再改回 AQ会(x)使基?CAS 的算法乱。在q种情况下,CAS 操作?x)成功,但是在一些情况下Q结果可能不是?zhn)所预期的。(注意Q?#160;清单 1 ?#160;清单 2 中的计数器和互斥例子不存在这个问题,但不是所有算法都q样。)(j)q类问题UCؓ(f) ABA 问题Q通常通过标记或版本~号与要q行 CAS 操作的每个值相兌Qƈ原子地更新值和标记Q来处理q类问题?#160;AtomicStampedReference
cL持这U方法?/td>
java.util.concurrent
包中的所有类都用原子变量,而不使用同步。类?code>ConcurrentLinkedQueue 的类也用原子变量直接实现无{待法Q而类?#160;ConcurrentHashMap
的类使用 ReentrantLock
在需要时q行锁定。然后, ReentrantLock
使用原子变量来维护等待锁定的U程队列?/p>
回页?/strong>
ReentrantLock
如何相对于同步提供可伸羃性优势,以及(qing)构造通过伪随机数生成器模拟旋转骰子的单、高竞争CZ基准。我向?zhn)昄了(jin)通过同步?#160;ReentrantLock
和公q?#160;ReentrantLock
来进行协调的实现Qƈ昄?jin)结果。本月,我将向该基准d其他实现Q?#160;AtomicLong
更新 PRNG 状态的实现?/p>
清单 5. 使用同步和原子变量实现线E安?PRNG
public class PseudoRandomUsingSynch implements PseudoRandom {
private int seed;
public PseudoRandomUsingSynch(int s) { seed = s; }
public synchronized int nextInt(int n) {
int s = seed;
seed = Util.calculateNext(seed);
return s % n;
}
}
public class PseudoRandomUsingAtomic implements PseudoRandom {
private final AtomicInteger seed;
public PseudoRandomUsingAtomic(int s) {
seed = new AtomicInteger(s);
}
public int nextInt(int n) {
for (;;) {
int s = seed.get();
int nexts = Util.calculateNext(s);
if (seed.compareAndSet(s, nexts))
return s % n;
}
}
}
ReentrantLock
与原子变量之间的q。?zhn)看刎ͼ虽?#160;ReentrantLock
拥有比同步更多的优点Q但相对?#160;ReentrantLock
Q原子变量提供了(jin)其他改进。(因ؓ(f)在每个工作单元中完成的工作很,所以下囑֏能无法完全地说明?ReentrantLock 相比Q原子变量具有哪些可伸羃性优炏V)(j)
?1. 8-way Ultrasparc3 中同步、ReentrantLock、公q?Lock ?AtomicLong 的基准吞吐量
?2. 单处理器 Pentium 4 中的同步、ReentrantLock、公q?Lock ?AtomicLong 的基准吞吐量
java.util.concurrent
中提供的版本Q如 ConcurrentLinkedQueue
。但是万一(zhn)想知道Ҏ(gu)以前 JDK 中的相类似的功能Q这些类的性能是如何改q的Q可以用通过原子变量cd开的细_度、硬件别的q发原语?/p>
回页?/strong>
java.util.concurrent
中的cd于这些低U原子变量工h建,为它们提供比以前执行怼功能的类更显著的可~性优炏V虽然?zhn)可能永远不?x)直接使用原子变量Q还是应该ؓ(f)它们的存在而欢呹{?/p>
q幅图中节点代表一?single ThreadQ边代表执行的步骤?/p>
整幅图代表的意思是QROOT U程执行完毕后执?T1 U程QT1 执行完毕后ƈ发的执行 T2 ?T3。而从 T2 ?T3 指向 T4 的两条边表示的是 T4 必须{?T2 ?T3 都执行完毕以后才能开始执行。剩下的步骤以此cLQ直?END 作ؓ(f)整个q程的结束。当?dng)q只是个略的C意图,可能面对的一个线E场景会(x)有上百个U程。还有,你可以观察到q整个场景只有一个入口点和一个出口点Q这意味着什么?在下文中Z解释?/p>
q其中涉?qing)到?Java U程的同步互斥机制。例如如何让 T1 ?T2 ?T3 之前q行Q如何让 T2 ?T3 都执行完毕之后开?T4 U程?/p>
如何来描q图 1 中所C的场景呢?可以采用 XML 的格式来描述我们的模型。我定义一?#8220;Thread” element 来表C线E?/p>
其中 ID 是线E的唯一标识W,PRETHREAD 便是该线E的直接先决U程的IDQ每个线E?ID 之间用逗号隔开?/p>
?Thread q个 element 里面可以加入你想要该U程执行d的具体信息?/p>
实际上模型的描述是解决问题非帔R要的一个环节,整个U程场景可以用一U一致的形式来描qͼ作ؓ(f) Java 多线Eƈ发控制框架引擎的输入。也是线E运行的模式?XML 来描q出来,q样只用改动 XML 配置文g可以更Ҏ(gu)个线E运行的模式Q不用改动Q何的源代码?/p>
对于 Java 多线E的q行框架来说Q我们将采用“?#8221;?#8220;?#8221;的两U模式来实现?/p>
Thread 是工作线E。ThreadEntry ?Thread 的包装类Qprerequisite 是一?HashMapQ它含有 Thread 的先决线E的状态。如?中显C的那样QT4 的先决线E是 T2 ?T3Q那?prerequisite 中就包含 T2 ?T3 的状态。TestScenario 中的 threadEntryList 中包含所有的 ThreadEntry?/p>
?3. U程执行场景
TestScenario 作ؓ(f)ȝE,作ؓ(f)一?#8220;?#8221;在的监控者,不断地轮?threadEntryList 中所?ThreadEntry 的状态,?ThreadEntry 接受?isReady 的查询后查询自己?prerequisiteQ当其中所有的先决U程的状态ؓ(f)“正常l束?#8221;Q它便返?readyQ那?TestScenario 便会(x)调用 ThreadEntry ?startThread() Ҏ(gu)授权?ThreadEntry q行U程QThread 侉K过 run() Ҏ(gu)来真正执行线E。ƈ在正常执行完毕后调用 setPreRequisteState() Ҏ(gu)来更新整?ScenarioQthreadEntryList 中所?ThreadEntry ?prerequisite 里面含有?Thread 的状态信息ؓ(f)“正常l束”?/p>
?4. 状态更改的q程
如图 1 中所C的 T4 的先决线Eؓ(f) T2 ?T3QT2 ?T3 q行执行。如?4 所C,假设 T2 先执行完毕,它会(x)调用 setPreRequisteState() Ҏ(gu)来更新整?ScenarioQ?threadEntryList 中所?ThreadEntry ?prerequisite 里面含有?T2 的状态信息ؓ(f)“正常l束”。此ӞT4 ?prerequisite ?T2 的状态ؓ(f)“正常l束”Q但?T3 q没有执行完毕,所以其状态ؓ(f)“未完?#8221;。所?T4 ?isReady 查询q回?falseQT4 不会(x)执行。只有当 T3 执行完毕后更新状态ؓ(f)“正常l束”后,T4 的状态才?readyQT4 才会(x)开始运行?/p>
其余的节点也以此cLQ它们正常执行完毕的时候会(x)在整个的 scenario 中广播该U程正常l束的信息,׃U程不断地轮询各?ThreadEntry 的状态来开启各个线E?/p>
q便是采用主控线E轮询状态表的方式来控制 Java 多线E运行框架的实现方式之一?/p>
优点Q?/strong>概念l构清晰明了(jin)Q实现简单。避免采?Java 的锁机制Q减生死锁的几率。当发生异常D其中某些U程不能正常执行完毕的时候,不会(x)产生挂v的线E?/p>
~点Q?/strong>采用ȝE轮询机Ӟ耗费 CPU 旉。当图中的节点太多的(n>??? 而线E单个线E执行时间比较短的时?t<??? 需要进一步研I?时候会(x)产生U程启动的些微gq,也就是说实时性能在极端情况下不好Q当然这可以另外写一文章来专门探讨?/p>
相对?#8220;?#8221;-ȝE轮询机制来_(d)“?#8221;采用的是自我控制q锁触发机制?/p>
Thread 中的 lock 为当?Thread ?lockQlockList 是一?HashMapQ持有其后U程?lock 的引用,getLock ?setLock 可以?lockList 中的 Lock q行操作。其中很重要的一个成员是 waitForCountQ这是一个引用计数。表明当前线E正在等待的先决U程的个敎ͼ例如?1 中所C的 T4Q在初始的情况下Q他{待的先决线E是 T2 ?T3Q那么它?waitForCount {于 2?/p>
当整个过E开始运行的时候,我们所有的U程 startQ但是每个线E所持的 lock 都处?wait 状态,U程都会(x)处于 waiting 的状态。此Ӟ我们?root thread 所持有的自w的 lock notifyQ这?root thread ׃(x)q行h。当 root ?run Ҏ(gu)执行完毕以后。它?x)检查其后箋U程?waitForCountQƈ其值减一。然后再ơ检?waitForCountQ如?waitForCount {于 0Q表C后箋U程的所有先决线E都已经执行完毕Q此时我?notify 该线E的 lockQ该后箋U程便可以从 waiting 的状态{换成?running 的状态。然后这个过E连锁递归的进行下去,整个q程便会(x)执行完毕?/p>
我们q是?T2QT3QT4 ZQ当q行 initThreadLock q程的时候,我们可以知道 T4 有两个直接先决线E?T2 ?T3Q所?T4 ?waitForCount {于 2。我们假?T3 先执行完毕,T2 仍然?running 的状态,此时他会(x)首先遍历其所有的直接后U程Qƈ他们的 waitForCount 减去 1Q此时他只有一个直接后l线E?T4Q于?T4 ?waitForCount 减去 1 以后值变?1Q不{于 0Q此时不?x)?T4 ?lock notifyQT4 l箋 waiting。当 T2 执行完毕之后Q他?x)执行?T3 相同的步骤,此时 T4 ?waitForCount {于 0QT2 ?notify T4 ?lockQ于?T4 ?waiting 状态{换成?running 状态。其他的节点也是怼的情c(din)?/p>
当然Q我们也可以整个过E的信息攑֜另外的一个全局对象中,所有的U程都去查找该全局对象来获取各自所需的信息,而不是采取这U分布式存储的方式?/p>
优点Q?/strong>采用 wait¬ify 机制而不采用轮询的机Ӟ不会(x)费CPU资源。执行效率较高。而且相对?#8220;?#8221;-ȝE轮询的机制来说实时性更好?/p>
~点Q?/strong>采用 Java U程 Object 的锁机制Q实现v来较为复杂。而且采取一U连锁触发的方式Q如果其中某些线E异常,?x)导致所有其后U程的挂赯(g)造成整个 scenario 的运行失败。ؓ(f)?jin)防止这U情늚发生Q我们还必须建立一套线E监控的机制来确保其正常q行?/p>
下面的图所要表辄是这样一U递归q代的概c(din)例如在? 中展C的那样QT1 q个节点表示的是一个线E。现在,忘掉U程q样一个概念,?T1 抽象Z个过E,惌它是一个银河系Q深入到 T1 中去Q它也是一个许多子q程的集合,q些子过E之间的关系模式如?1 所C那P可以用一个图来表C?/p>
可以惌一下这是怎样的一个框Ӟh无穷扩展性的q程框架Q我们只用定义各个过E之间的关系Q我们不用关?j)过E是怎样q行的。事实上Q可以在最l的节点上指定一个实际的工作Q比如读一个文Ӟ或者submit一个JCL jobQ或者执行一条sql statement?/p>
其实Q按照某U遍历规则,完全可以这U嵌套递归的结构{化成Z个一层扁q结构的图,而不是原来的分层的网状结构,但是我们不这样做的原因是Z以下的几点考虑Q?/p>
实际上,q是一个状态聚集的层次控制框架Q我们可以依赖此框架来执行自主运。我们将在其它的文章中来讨论它的应用?/p>
本文介绍?jin)一U?Java 多线Eƈ发控制的框架Qƈl出?jin)其两种实现的模型,它们有各自的优缺点,有各自的适用范围。当需要进?Java U程的ƈ发控制的时候,可以作ؓ(f)参考?/p>
陈威Q华中科技大学士QIBM CSDL Software EngineerQ所在的 Team ?DB2 for z/OS。联pL式:(x)chenwbj@cn.ibm.com
?5. 锁机制的静态类?/strong>
?6. 锁机制执行顺序图
?7. 嵌套子过E?/strong>
For information about ongoing work on the memory model, see Bill Pugh's Java Memory Model pages.
Consider the tiny class, defined without any synchronization:
final class SetCheck { private int a = 0; private long b = 0; void set() { a = 1; b = -1; } boolean check() { return ((b == 0) || (b == -1 && a == 1)); } }In a purely sequential language, the method check could never return false. This holds even though compilers, run-time systems, and hardware might process this code in a way that you might not intuitively expect. For example, any of the following might apply to the execution of method set:
Things are different in concurrent programming. Here, it is entirely possible for check to be called in one thread while set is being executed in another, in which case the check might be "spying" on the optimized execution of set. And if any of the above manipulations occur, it is possible for check to return false. For example, as detailed below, check could read a value for the long b that is neither 0 nor -1, but instead a half-written in-between value. Also, out-of-order execution of the statements in set may cause check to read b as -1 but then read a as still 0.
In other words, not only may concurrent executions be interleaved, but they may also be reordered and otherwise manipulated in an optimized form that bears little resemblance to their source code. As compiler and run-time technology matures and multiprocessors become more prevalent, such phenomena become more common. They can lead to surprising results for programmers with backgrounds in sequential programming (in other words, just about all programmers) who have never been exposed to the underlying execution properties of allegedly sequential code. This can be the source of subtle concurrent programming errors.
In almost all cases, there is an obvious, simple way to avoid contemplation of all the complexities arising in concurrent programs due to optimized execution mechanics: Use synchronization. For example, if both methods in class SetCheck are declared as synchronized, then you can be sure that no internal processing details can affect the intended outcome of this code.
But sometimes you cannot or do not want to use synchronization. Or perhaps you must reason about someone else's code that does not use it. In these cases you must rely on the minimal guarantees about resulting semantics spelled out by the Java Memory Model. This model allows the kinds of manipulations listed above, but bounds their potential effects on execution semantics and additionally points to some techniques programmers can use to control some aspects of these semantics (most of which are discussed in K?).
The Java Memory Model is part of The JavaTM Language Specification, described primarily in JLS chapter 17. Here, we discuss only the basic motivation, properties, and programming consequences of the model. The treatment here reflects a few clarifications and updates that are missing from the first edition of JLS.
The assumptions underlying the model can be viewed as an idealization of a standard SMP machine of the sort described in K?.4:
For purposes of the model, every thread can be thought of as running on a different CPU from any other thread. Even on multiprocessors, this is infrequent in practice, but the fact that this CPU-per-thread mapping is among the legal ways to implement threads accounts for some of the model's initially surprising properties. For example, because CPUs hold registers that cannot be directly accessed by other CPUs, the model must allow for cases in which one thread does not know about values being manipulated by another thread. However, the impact of the model is by no means restricted to multiprocessors. The actions of compilers and processors can lead to identical concerns even on single-CPU systems.
The model does not specifically address whether the kinds of execution tactics discussed above are performed by compilers, CPUs, cache controllers, or any other mechanism. It does not even discuss them in terms of classes, objects, and methods familiar to programmers. Instead, the model defines an abstract relation between threads and main memory. Every thread is defined to have a working memory (an abstraction of caches and registers) in which to store values. The model guarantees a few properties surrounding the interactions of instruction sequences corresponding to methods and memory cells corresponding to fields. Most rules are phrased in terms of when values must be transferred between the main memory and per-thread working memory. The rules address three intertwined issues:
When synchronization is not used or is used inconsistently, answers become more complex. The guarantees made by the memory model are weaker than most programmers intuitively expect, and are also weaker than those typically provided on any given JVM implementation. This imposes additional obligations on programmers attempting to ensure the object consistency relations that lie at the heart of exclusion practices: Objects must maintain invariants as seen by all threads that rely on them, not just by the thread performing any given state modification.
The most important rules and properties specified by the model are discussed below.
Atomicity guarantees ensure that when a non-long/double field is used in an expression, you will obtain either its initial value or some value that was written by some thread, but not some jumble of bits resulting from two or more threads both trying to write values at the same time. However, as seen below, atomicity alone does not guarantee that you will get the value most recently written by any thread. For this reason, atomicity guarantees per se normally have little impact on concurrent program design.
In essence, releasing a lock forces a flush of all writes from working memory employed by the thread, and acquiring a lock forces a (re)load of the values of accessible fields. While lock actions provide exclusion only for the operations performed within a synchronized method or block, these memory effects are defined to cover all fields used by the thread performing the action.
Note the double meaning of synchronized: it deals with locks that permit higher-level synchronization protocols, while at the same time dealing with the memory system (sometimes via low-level memory barrier machine instructions) to keep value representations in synch across threads. This reflects one way in which concurrent programming bears more similarity to distributed programming than to sequential programming. The latter sense of synchronized may be viewed as a mechanism by which a method running in one thread indicates that it is willing to send and/or receive changes to variables to and from methods running in other threads. From this point of view, using locks and passing messages might be seen merely as syntactic variants of each other.
Among other consequences, it is bad practice to make available the reference to an incompletely constructed object (see K?.2). It can also be risky to start new threads inside a constructor, especially in a class that may be subclassed. Thread.start has the same memory effects as a lock release by the thread calling start, followed by a lock acquire by the started thread. If a Runnable superclass invokes new Thread(this).start() before subclass constructors execute, then the object might not be fully initialized when the run method executes. Similarly, if you create and start a new thread T and then create an object X used by thread T, you cannot be sure that the fields of X will be visible to T unless you employ synchronization surrounding all references to object X. Or, when applicable, you can create X before starting T.
The memory model guarantees that, given the eventual occurrence of the above operations, a particular update to a particular field made by one thread will eventually be visible to another. But eventually can be an arbitrarily long time. Long stretches of code in threads that use no synchronization can be hopelessly out of synch with other threads with respect to values of fields. In particular, it is always wrong to write loops waiting for values written by other threads unless the fields are volatile or accessed via synchronization (see K?.6).
The model also allows inconsistent visibility in the absence of synchronization. For example, it is possible to obtain a fresh value for one field of an object, but a stale value for another. Similarly, it is possible to read a fresh, updated value of a reference variable, but a stale value of one of the fields of the object now being referenced.
However, the rules do not require visibility failures across threads, they merely allow these failures to occur. This is one aspect of the fact that not using synchronization in multithreaded code doesn't guarantee safety violations, it just allows them. On most current JVM implementations and platforms, even those employing multiple processors, detectable visibility failures rarely occur. The use of common caches across threads sharing a CPU, the lack of aggressive compiler-based optimizations, and the presence of strong cache consistency hardware often cause values to act as if they propagate immediately among threads. This makes testing for freedom from visibility-based errors impractical, since such errors might occur extremely rarely, or only on platforms you do not have access to, or only on those that have not even been built yet. These same comments apply to multithreaded safety failures more generally. Concurrent programs that do not use synchronization fail for many reasons, including memory consistency problems.
Note that the within-thread point of view is implicitly adopted in all other discussions of semantics in JLS. For example, arithmetic expression evaluation is performed in left-to-right order (JLS section 15.6) as viewed by the thread performing the operations, but not necessarily as viewed by other threads.
The within-thread as-if-serial property is helpful only when only one thread at a time is manipulating variables, due to synchronization, structural exclusion, or pure chance. When multiple threads are all running unsynchronized code that reads and writes common fields, then arbitrary interleavings, atomicity failures, race conditions, and visibility failures may result in execution patterns that make the notion of as-if-serial just about meaningless with respect to any given thread.
Even though JLS addresses some particular legal and illegal reorderings that can occur, interactions with these other issues reduce practical guarantees to saying that the results may reflect just about any possible interleaving of just about any possible reordering. So there is no point in trying to reason about the ordering properties of such code.
final class VFloat { private float value; final synchronized void set(float f) { value = f; } final synchronized float get() { return value; } }Declaring a field as volatile differs only in that no locking is involved. In particular, composite read/write operations such as the "++'' operation on volatile variables are not performed atomically.
Also, ordering and visibility effects surround only the single access or update to the volatile field itself. Declaring a reference field as volatile does not ensure visibility of non-volatile fields that are accessed via this reference. Similarly, declaring an array field as volatile does not ensure visibility of its elements. Volatility cannot be manually propagated for arrays because array elements themselves cannot be declared as volatile.
Because no locking is involved, declaring fields as volatile is likely to be cheaper than using synchronization, or at least no more expensive. However, if volatile fields are accessed frequently inside methods, their use is likely to lead to slower performance than would locking the entire methods.
Declaring fields as volatile can be useful when you do not need locking for any other reason, yet values must be accurately accessible across multiple threads. This may occur when:
大多数编E语a的语a规范都不?x)谈到线E和q发的问题;因ؓ(f)一直以来,q些问题都是留给q_或操作系l去详细说明的。但是,Java 语言规范QJLSQ却明确包括一个线E模型,q提供了(jin)一些语a元素供开发h员用以保证他们E序的线E安全?/p>
对线E的明确支持有利也有弊。它使得我们在写E序时更Ҏ(gu)利用U程的功能和便利Q但同时也意味着我们不得不注意所写类的线E安全,因ؓ(f)Mc都很有可能被用在一个多U程的环境内?/p>
许多用户W一ơ发C们不得不ȝ解线E的概念的时候,q不是因Z们在写创建和理U程的程序,而是因ؓ(f)他们正在用一个本w是多线E的工具或框架。Q何用q?Swing GUI 框架或写q小服务E序?JSP 늚开发h员(不管有没有意识到Q都曄被线E的复杂性困扰过?/p>
Java 设计师是惛_ZU语aQ之能够很好地q行在现代的gQ包括多处理器系l上。要辑ֈq一目的Q管理线E间协调的工作主要推l了(jin)软g开发h员;E序员必L定线E间׃n数据的位|。在 Java E序中,用来理U程间协调工作的主要工具?synchronized
关键字。在~少同步的情况下QJVM 可以很自由地对不同线E内执行的操作进行计时和排序。在大部分情况下Q这正是我们惌的,因ؓ(f)q样可以提高性能Q但它也l程序员带来?jin)额外的负担Q他们不得不自己识别什么时候这U性能的提高会(x)危及(qing)E序的正性?/p>
大部?Java E序员对同步的块或方法的理解是完全根据用互斥(互斥信号量)(j)或定义一个(f)界段Q一个必d子性地执行的代码块Q。虽?synchronized
的语义中实包括互斥和原子性,但在程q入之前和在程退Z后发生的事情要复杂得多?/p>
synchronized
的语义确实保证了(jin)一ơ只有一个线E可以访问被保护的区D,但同时还包括同步U程在主存内互相作用的规则。理?Java 内存模型QJMMQ的一个好Ҏ(gu)是把各个线E想像成q行在相互分ȝ处理器上Q所有的处理器存取同一块主存空_(d)每个处理器有自己的缓存,但这些缓存可能ƈ不dd同步。在~少同步的情况下QJMM ?x)允怸个线E在同一个内存地址上看C同的倹{而当用一个管E(锁)(j)q行同步的时候,一旦申请加?jin)锁QJMM ׃(x)马上要求该缓存失效,然后在它被释攑։对它q行hQ把修改q的内存位置写回dQ。不隄Zؓ(f)什么同步会(x)对程序的性能影响q么大;频繁地刷新缓存代价会(x)很大?/p>
![]() ![]() |
![]()
|
如果同步不适当Q后果是很严重的Q会(x)造成数据混ؕ和争用情况,DE序崩溃Q生不正确的结果,或者是不可预计的运行。更p的是,q些情况可能很少发生且具有偶然性(使得问题很难被监和重现Q。如果测试环境和开发环境有很大的不同,无论是配|的不同Q还是负L(fng)不同Q都有可能得这些问题在试环境中根本不出现Q从而得出错误的l论Q我们的E序是正的Q而事实上q些问题只是q没出现而已?/p>
![]() |
|
另一斚wQ不当或q度C用同步会(x)D其它问题Q比如性能很差和死锁。当?dng)性能差虽然不如数据乱那么严重,但也是一个严重的问题Q因此同样不可忽视。编写优U的多U程E序需要用好的运行\U,_的同步可以(zhn)的数据不发生乱,但不需要滥用到L担死锁或不必要地削弱E序性能的风险?/p>
![]() ![]() |
![]()
|
׃包括~存h和设|失效的q程QJava 语言中的同步块通常比许多^台提供的临界D设备代h大,q些临界D通常是用一个原子性的“test and set bit”机器指o(h)实现的。即使一个程序只包括一个在单一处理器上q行的单U程Q一个同步的Ҏ(gu)调用仍要比非同步的方法调用慢。如果同步时q发生锁定争用,那么性能上付出的代h(hun)?x)大得多Q因Z(x)需要几个线E切换和pȝ调用?/p>
q运的是Q随着每一版的 JVM 的不断改q,既提高了(jin) Java E序的M性能Q同时也相对减少?jin)同步的代h(hun)Qƈ且将来还可能?x)有q一步的改进。此外,同步的性能代h(hun)l常是被夸大的。一个著名的资料来源曾l引证说一个同步的Ҏ(gu)调用比一个非同步的方法调用慢 50 倍。虽然这句话有可能是真的Q但也会(x)产生误导Q而且已经D?jin)许多开发h员即使在需要的时候也避免使用同步?/p>
严格依照癑ֈ比计同步的性能损失q没有多大意义,因ؓ(f)一个无争用的同步给一个块或方法带来的是固定的性能损失。而这一固定的gq带来的性能损失癑ֈ比取决于在该同步块内做了(jin)多少工作。对一?em>I?/em>Ҏ(gu)的同步调用可能要比对一个空Ҏ(gu)的非同步调用?20 倍,但我们多长时间才调用一ơ空Ҏ(gu)呢?当我们用更有代表性的方法来衡量同步损失Ӟ癑ֈ数很快就下降到可以容忍的范围之内?/p>
?1 把一些这U数据放在一h看。它列D?jin)一些不同的实例Q不同的q_和不同的 JVM 下一个同步的Ҏ(gu)调用相对于一个非同步的方法调用的损失。在每一个实例下Q我q行一个简单的E序Q测定@环调用一个方?10Q?00Q?00 ơ所需的运行时_(d)我调用了(jin)同步和非同步两个版本Qƈ比较?jin)结果。表g的数据是同步版本的运行时间相对于非同步版本的q行旉的比率;它显CZ(jin)同步的性能损失。每ơ运行调用的都是清单 1 中的单方法之一?/p>
表格 1 中显CZ(jin)同步Ҏ(gu)调用相对于非同步Ҏ(gu)调用的相Ҏ(gu)能Qؓ(f)?jin)用l对的标准测定性能损失Q必考虑?JVM 速度提高的因素,qƈ没有在数据中体现出来。在大多数测试中Q每?JVM 的更高版本都?x)?JVM 的M性能得到很大提高Q很有可?1.4 版的 Java 虚拟机发行的时候,它的性能q会(x)有进一步的提高?/p>
JDK | staticEmpty | empty | fetch | hashmapGet | singleton | create |
Linux / JDK 1.1 | 9.2 | 2.4 | 2.5 | n/a | 2.0 | 1.42 |
Linux / IBM Java SDK 1.1 | 33.9 | 18.4 | 14.1 | n/a | 6.9 | 1.2 |
Linux / JDK 1.2 | 2.5 | 2.2 | 2.2 | 1.64 | 2.2 | 1.4 |
Linux / JDK 1.3 (no JIT) | 2.52 | 2.58 | 2.02 | 1.44 | 1.4 | 1.1 |
Linux / JDK 1.3 -server | 28.9 | 21.0 | 39.0 | 1.87 | 9.0 | 2.3 |
Linux / JDK 1.3 -client | 21.2 | 4.2 | 4.3 | 1.7 | 5.2 | 2.1 |
Linux / IBM Java SDK 1.3 | 8.2 | 33.4 | 33.4 | 1.7 | 20.7 | 35.3 |
Linux / gcj 3.0 | 2.1 | 3.6 | 3.3 | 1.2 | 2.4 | 2.1 |
Solaris / JDK 1.1 | 38.6 | 20.1 | 12.8 | n/a | 11.8 | 2.1 |
Solaris / JDK 1.2 | 39.2 | 8.6 | 5.0 | 1.4 | 3.1 | 3.1 |
Solaris / JDK 1.3 (no JIT) | 2.0 | 1.8 | 1.8 | 1.0 | 1.2 | 1.1 |
Solaris / JDK 1.3 -client | 19.8 | 1.5 | 1.1 | 1.3 | 2.1 | 1.7 |
Solaris / JDK 1.3 -server | 1.8 | 2.3 | 53.0 | 1.3 | 4.2 | 3.2 |
public void empty() { }
public Object fetch() { return field; }
public Object singleton() {
if (singletonField == null)
singletonField = new Object();
return singletonField;
}
public Object hashmapGet() {
return hashMap.get("this");
}
public Object create() {
return new Object();
}
q些基准测试也阐明?jin)存在动态编译器的情况下解释性能l果所面(f)的挑战。对?1.3 JDK 在有和没?JIT Ӟ数字上的巨大差异需要给Z些解释。对那些非常单的Ҏ(gu)Q?empty
?fetch
Q,基准试的本质(它只是执行一个几乎什么也不做的紧凑的循环Q?JIT 可以动态地~译整个循环Q把q行旉压羃到几乎没有的地步。但在一个实际的E序中,JIT 能否q样做就要取决于很多因素?jin),所以,?JIT 的计时数据可能在做公q_比时更有用一些。在M情况下,对于更充实的Ҏ(gu)Q?create
?hashmapGet
Q,JIT ׃能象Ҏ(gu)单些的方法那样非同步的情况得到巨大的改q。另外,从数据中看不?JVM 是否能够Ҏ(gu)试的重要部分q行优化。同P在可比较?IBM ?Sun JDK 之间的差异反映了(jin) IBM Java SDK 可以更大E度C化非同步的@环,而不是同步版本代h高。这在纯计时数据中可以明昑֜看出Q这里不提供Q?/p>
从这些数字中我们可以得出以下l论Q对非争用同步而言Q虽然存在性能损失Q但在运行许多不是特别微的Ҏ(gu)Ӟ损失可以降到一个合理的水^Q大多数情况下损失大概在 10% ?200% 之间Q这是一个相对较?yu)的数目Q。所以,虽然同步每个Ҏ(gu)是不明智的(q也?x)增加死锁的可能性)(j)Q但我们也不需要这么害怕同步。这里用的单测试是说明一个无争用同步的代仯比创Z个对象或查找一?code>HashMap 的代价小?/p>
׃早期的书c和文章暗示?jin)无争用同步要付出巨大的性能代h(hun)Q许多程序员q全力避免同步。这U恐惧导致了(jin)许多有问题的技术出玎ͼ比如?double-checked lockingQDCLQ。许多关?Java ~程的书和文章都推荐 DCLQ它看上ȝ是避免不必要的同步的一U聪明的Ҏ(gu)Q但实际上它Ҏ(gu)没有用,应该避免使用它。DCL 无效的原因很复杂Q已出?jin)本文讨论的范围Q要深入?jin)解Q请参阅 参考资?/a>里的链接Q?/p>
![]() ![]() |
![]()
|
假设同步使用正确Q若U程真正参与争用加锁Q?zhn)也能感受到同步对实际性能的媄(jing)响。ƈ且无争用同步和争用同步间的性能损失差别很大Q一个简单的试E序指出争用同步比无争用同步?50 倍。把q一事实和我们上面抽取的观察数据l合在一P可以看出使用一个争用同步的代h(hun)臛_相当于创?50 个对象?/p>
所以,在调试应用程序中同步的用时Q我们应该努力减实际争用的数目Q而根本不是简单地试图避免使用同步。这个系列的W?2 部分把重点攑֜减少争用的技术上Q包括减锁的粒度、减同步块的大以?qing)减线E间׃n数据的数量?/p>
![]() ![]() |
![]()
|
要(zhn)的E序U程安全Q首先必ȝ定哪些数据将在线E间׃n。如果正在写的数据以后可能被另一个线E读刎ͼ或者正在读的数据可能已l被另一个线E写q了(jin)Q那么这些数据就是共享数据,必须q行同步存取。有些程序员可能?x)惊讶地发现Q这些规则在单地(g)查一个共享引用是否非I的时候也用得上?/p>
许多Z(x)发现q些定义惊hC根{有一U普遍的观点是,如果只是要读一个对象的字段Q不需要请求加锁,其是在 JLS 保证?32 位读操作的原子性的情况下,它更是如此。但不幸的是Q这个观Ҏ(gu)错误的。除非所指的字段被声明ؓ(f) 在确定了(jin)要共享的数据之后Q还要确定要如何保护那些数据。在单情况下Q只需把它们声明ؓ(f) q有一点值得注意的是Q简单地同步存取器方法(或声明下层的字段?
如果一个调用者想要增?
如果两个U程试图同时增加 以上情况是一个很好的CZQ说明我们应该注意多层次_度的数据完整性;同步存取器方法确保调用者能够存取到一致的和最q版本的属性|但如果希望属性的来g当前g_(d)或多个属性间怺一_(d)我们必d步复合操??可能是在一个粗_度的锁上?/p>
volatile
Q否?JMM 不会(x)要求下面的^台提供处理器间的~存一致性和序q诏性,所以很有可能,在某些^CQ没有同步就?x)读到陈旧的数据。有x详细的信息,请参?参考资?/a>?/p>
volatile
卛_保护数据字段Q在其它情况下,必须在读或写׃n数据前请求加锁,一个很好的l验是明指Z用什么锁来保护给定的字段或对象,q在你的代码里把它记录下来?/p>
volatile
Q可能ƈ不以保护一个共享字Dc(din)可以考虑下面的示例:(x)
private int foo;
public synchronized int getFoo() { return foo; }
public synchronized void setFoo(int f) { foo = f; }foo
属性|以下完成该功能的代码׃是线E安全的Q?/p>
setFoo(getFoo() + 1);foo
属性|l果可能?#160;foo
的值增加了(jin) 1 ?2Q这p时决定。调用者将需要同步一个锁Q才能防止这U争用情况;一个好Ҏ(gu)是在 JavaDoc cM指定同步哪个锁,q样cȝ调用者就不需要自q?jin)?/p>
![]() ![]() |
![]()
|
有时Q在写一个类的时候,我们q不知道它是否要用在一个共享环境里。我们希望我们的cLU程安全的,但我们又不希望给一个L在单U程环境内用的cd上同步的负担Q而且我们可能也不知道使用q个cL合适的锁粒度是多大。幸q的是,通过提供同步包装Q我们可以同时达C上两个目的。Collections cd是这U技术的一个很好的CZQ它们是非同步的Q但在框架中定义的每个接口都有一个同步包装(例如Q?#160;Collections.synchronizedMap()
Q,它用一个同步的版本来包装每个方法?/p>
![]() ![]() |
![]()
|
虽然 JLS l了(jin)我们可以使我们的E序U程安全的工P但线E安全也不是天上掉下来的馅饼。用同步会(x)蒙受性能损失Q而同步用不当又?x)我们承担数据混ؕ、结果不一致或死锁的风险。幸q的是,在过ȝ几年?JVM 有了(jin)很大的改q,大大减少?jin)与正确使用同步相关的性能损失。通过仔细分析在线E间如何׃n数据Q适当地同步对׃n数据的操作,可以使得(zhn)的E序既是U程安全的,又不?x)承受过多的性能负担?/p>
synchronized
的实际意义的著作?#160;![]() |
||
![]() |
Brian Goetz 是一名Y仉问,q且q去 15 q来一直是专业的Y件开发h员。他?#160;QuiotixQ一家坐落在 Los AltosQCalifornia 的Y件开发和咨询公司的首席顾问。请通过 brian@quiotix.com ?Brian 联系?/p> |