Java硬件对并发的支持

    xiaoxiao2025-04-10  14

        独占锁是一项悲观技术--它假设最坏的情况(如果不锁门,那么捣蛋鬼就会闯入并搞的一团糟),并且只有在确保其它线程不会造成干扰(通过获取正确的锁)的情况下才能执行下去。

        在早期的处理器中支持原子的测试并设置(Test-and-Set),获取并递增(Fetch-and-Increment)以及交换(Swap)等指令。这些命令足以实现各种互斥体,而这些互斥体又可以实现一些更复杂的并发对象。现在,几乎所有的现代处理器中都包含了某种形式的原子读-改-写指令,例如比较并交换(Compare-and-Swap)或者关联加载/条件存储(Load-Linked/Store-Conditional)。

    1.比较并交换

        在大多数处理器架构(包括IA32和Sparc)中采用的方法是实现一个比较并交换(CAS)指令。(在其它处理器中,例如PowerPC,采用一对指令来实现相同的功能:关联加载与条件存储。)CAS包含了3个操作数--需要读写的内容位置V、进行比较的值A和拟写入的新值B。当且仅当V的值等于A时,CAS才会通过原子方式用新值B来更新V的值,否则不会执行任何操作。无论位置V的值是否等于A,都将返回V原有的值。CAS是一项乐观的技术,它希望能成功地执行更新操作,并且如果有另一个线程在最近一次检查后更新了该变量,那么CAS能检测到这个错误。

    模拟CAS操作代码如下:

    @ThreadSafe public class SimulatedCAS { @GuardedBy("this") private int value; public synchronized int get(){ return value; } public synchronized int compareAndSwap(int exceptedValue,int newValue){ //比较并交换,返回设置之前的值 int oldValue = value; if(oldValue==exceptedValue){ value = newValue; } return oldValue; } public synchronized boolean compareAndSet(int expectedValue,int newValue){ //比较并设置,返回true或false return (expectedValue==compareAndSwap(expectedValue,newValue)); } }

    当多个线程尝试使用CAS同时更新一个变量时,只有其中一个线程能更新变量的值,而其它线程都将失败。然而,失败的线程并不会被挂起(这与获取锁的情况不同,当获取锁失败时,线程将被挂起 ),而是被告知在这次竞争中失败,并可以再次尝试。

        CAS的典型使用模式是:首先从V中读取值A,并根据A计算新值B,然后再通过CAS以原子方式将V中的值由A变成B(只要在这期间没有任何线程将V的值修改为其它值)。由于CAS能检测到来自其它线程的干扰,因此即使不适用锁也能够实现原子的读-改-写操作序列。

    2.非阻塞的计数器

        如下代码使用CAS实现了一个线程安全的计数器。递增操作采用了标准形式--读取旧的值,根据它计算出新值(加1),并使用CAS来设置这个新值。如果CAS失败,那么该操作将立即重试。通常,反复地重试是一种合理的策略,但在一些很激烈的情况下,更好的方式是在重试之前首先等待一段时间或者回退,从而避免造成活锁问题。

    public class CasCounter { private SimulatedCAS value; public int getValue(){ return value.get(); } public int increment(){ int v; do{ v = value.get(); }while(v!=value.compareAndSwap(v, v+1)); return v+1; } } 在实现情况中,如果仅需要一个计数器或序列生成器,那么可以直接使用AtomicInteger或AtomicLong,它们能提供原子的递增方法以及其它算数方法。

        实际上,当竞争程度不高时,基于CAS的计数器在性能上远远超过了基于锁的计数器,而在没有竞争时甚至更高。如果要快速获取无竞争的锁,那么至少需要一次CAS操作再加上与其它锁相关的操作,因此基于锁的计数器即使在最好的情况下也会比基于CAS的计数器在一般情况下能执行更多的操作。由于CAS在大多数情况下都能执行成功(假设竞争程度不高),因此硬件能够正确地预测while循环中的分支,从而把复杂控制逻辑的开销降至最低。

        虽然Java语言的锁定语法比较简洁,但JVM和操作在管理锁时需要完成的工作却并不简单。在实现锁定时需要遍历JVM中一条非常复杂的代码路径,并可能导致操作系统级的锁定、线程挂起以及上下文切换等操作。在最好的情况下,在锁定时至少需要一次CAS,因此虽然在使用锁时没有用到CAS,但实际上也无法节约任何执行开销。另一方面,在程序内部执行CAS时不需要执行JVM代码、系统调用或线程调度操作。在应用级上看起来越长的代码路径,如果加上JVM和操作系统的代码调用,那么事实上却变得更短。CAS的主要缺点是,它将使调用者处理竞争问题(通过重试、回退、放弃),而在锁中能自动处理竞争问题(线程在获取锁之前将一直阻塞)。

        CAS的性能会随着处理器数量的不同而变化很大。在单CPU系统中,CAS通常只需要很少的时钟周期,因此不需要处理器之间的同步。非竞争的CAS在多CPU系统中需要10到150个时钟周期的开销。CAS的执行性能不仅在不同的体系架构之间变化很大,甚至在相同处理器的不同版本之间也会发生改变。一个很管用的法则是:在大多数处理器上,在无竞争的锁获取和释放的“快速代码路径”上的开销,大约是CAS开销的两倍。

    3.JVM对CAS的支持

        那么,Java代码如何确保处理器执行CAS操作?在Java5.0之前,如果不编写明确的代码,那么就无法执行CAS。在Java5.0中引入了底层的支持,在int、long和对象的引用等类型上都松开了CAS操作,并且JVM把他们编译为底层硬件提供的最有效方法。在支持CAS的平台上,运行时把它们编译为相应的(多条)机器指令。在最坏的情况下,如果不支持CAS指令,那么JVM将使用自旋锁。在原子变量类(例如java.util.concurrent.atomic中的AtomicXxx)中使用了这些底层的JVM支持为数字类型和引用类型提供一种高效的CAS操作,而在java.util.concurrent中的大多数类在实现时则直接或间接地使用了这些原子变量类。

    转载请注明原文地址: https://ju.6miu.com/read-1297918.html
    最新回复(0)