`
海浪儿
  • 浏览: 274610 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

自旋锁和缓存一致性

阅读更多

本文为原创,转载请注明出处

1、两种自旋锁的实现:TAS和TTAS

1.1 TAS

class TasLock {
        AtomicBoolean state = new AtomicBoolean(false);

        void lock() {
            while (state.getAndSet(true)) {
            }
        }

        void unLock() {
            state.set(false);
        }
    }

       AtomicBoolean的getAndSet方法是原子的,TAS循环执行getAndSet方法,当发现原来的值为false,且设置新值为true成功后,则认为获取到了锁。

1.2 TTAS

class TtasLock{
        AtomicBoolean state = new AtomicBoolean(false);

        void lock() {
            while(true){
                while(state.get()){}
                if(!state.getAndSet(true)){
                    return;
                }
            }
        }

        void unLock() {
            state.set(false);
        }
    }

       TTAS与TAS在功能的正确性上是一致的,但在实现手法上的区别在于,TTAS首先循环调用state.get()方法,只有当返回结果为false,即此时该锁还没有被其他线程占有时,才会再去调用getAndSet去获取锁

 

2、两种自旋锁的性能比较

基于TAS和TTAS设计锁,然后用多线程(10线程并发)累加一个公共的计数器。

public class MyTest {
	//并发线程数
	private final static int NTHREAD = 10;
	//计数
	private static volatile int count = 0;
	//最大计数
	private final static int MAX = 100000000;
	//TAS锁
	private static TASLock lock = new TASLock();
	//TTAS锁
	//private static TTASLock lock = new TTASLock();
	
	public static void main(String[] args) throws InterruptedException {
		final CountDownLatch endGate = new CountDownLatch(1);
		final CountDownLatch startGate = new CountDownLatch(1);
		ExecutorService e = Executors.newFixedThreadPool(NTHREAD);
		for (int i = 0; i < NTHREAD; i++) {
			e.execute(new Runnable() {
				@Override
				public void run() {
					try {
						startGate.await();
						while (true) {
							lock.lock();
							if (count < MAX) {
								count++;
								lock.unLock();
								continue;
							}
							lock.unLock();
							break;
						}
						endGate.countDown();
					} catch (InterruptedException ignored) {
					}

				}
			});
		}
		long start = System.currentTimeMillis();
		startGate.countDown();
		endGate.await();
		long end = System.currentTimeMillis();
		long time = end - start;
		e.shutdown();
		System.out.print(time);
	}
}
说明:采用了闭锁CountDownLatch,以便让10个并发线程同时开始计数

在我的机器上(4核)运行多次,统计得到的大致结果对比如下:

 

第一次

第二次

第三次

第四次

第五次

平均耗时(ms)

TAS

27473

28288

28288

26151

26595

27359

TTAS

25587

26115

26115

26063

25182

25812.4

单线程

1594

1598

1592

1601

1597

1596.4

分析:

       可以看出无论是TAS还是TTAS,性能均远远差于单线程的版本,这是合理的,因为并行是否能带来性能的提升,关键取决于执行的任务中串行任务所占的比例。而本例中,由于锁的存在,使得串行所占整个任务的比例为100%,因此即使线程再多,也只能串行执行,而多线程带来的上下文切换又会消耗一定的时间,所以多线程的性能更差。

       但是,为什么TTAS会比TAS的性能要好呢?TTAS是循环执行state.get()方法,TAS是循环执行state.getAndSet(true)方法,这两种实现手法带来的本质区别是什么?要解释这一点,就必须先了解现代多处理器的缓存架构

 

3、现代多处理器缓存架构



       所有的处理器共享一个主存(memory),主存的访问速度很慢,需要消耗50~100个cycles,每个处理器都需要通过同一个bus同memory和其他处理器通信,而同一时刻只允许一个处理器通过bus发送消息(但memory和所有的处理器都能同时监听过bus的消息)。由于memory访问速度慢,而且还可能会等待,所以每个处理器都有自己的局部缓存(cache)。cache的访问速度很快,只需要消耗1~2个cycles,而且不会出现等待。

       但天下没有免费的晚餐,现在同一主存地址对应的数据可能会在多个cache中存储,任何一个处理器都可能会修改这份数据,那么如何保证cache之间、cache与Memory之间的数据的一致性呢?由缓存一致性协议来保证,MESI协议就是其中最著名的一种。

MESI协议将Cache line分为四种状态:

Modified:该cache line已经被修改,且只被一个处理器的cache缓存,最终必须写回主存;

Exclusive:该cache line 只被一个处理器的cache缓存,未被修改,与主存中的数据一致。

Shared:该cache line被多个处理器的cache缓存,未被修改,与主存中的数据一致

Invalid:该cache line已经失效

下面以一个图例解释MESI协议的执行:

 a)        处理器A在bus上发出读取地址a处的数据的消息。由于只有主存有该数据,所以主存监听到该消息后,将地址a处的数据给处理器A,处理器A将数据缓存到自己的cache中,cache line的状态为Exclusive;

b)        处理器B在bus上发出读取同样地址a处的数据,处理器A监听到了该消息,将之前缓存的地址a处的数据给处理器B。此时,cache line的状态都变为Shared;

c)        处理器B修改地址a处的数据,将自己cache中的cache line的状态修改为Modified。同时,在bus上发出地址a处的数据被修改的消息,处理器A监听到该消息,将自己cache中cache line的状态修改为Invalid;

d)   处理器A在bus上发出重新读取地址a处的数据的消息,处理器B监听到该消息后,将修改后的数据同时发给处理器A和主存。处理器A和B的cache中的cache line的状态都变为Shared。

 

4、TTAS为什么比TAS要快

       在TAS中,每次执行锁的getAndSet(true)方法,会独占bus以发送锁的state变量被修改的消息,导致其他处理器执行getAndSet(true)时都需要等待,出现了串行。更重要的是,有可能此时占有锁的处理器正准备执行set(false)方法以释放锁,但由于bus一直被执行getAndSet(true)方法的处理器占用着,导致释放锁也被延迟。

       而在TTAS中,只有当get()方法返回false的情况下,才会执行getAndSet(true)方法。只要state没有被修改,处理器都在各自的cache中局部自旋着,不会占用bus,因此不会造成锁的释放被延迟。

当然,TTAS也不是最完美的,当处理器释放锁执行set(false)方法后,所有其他处理器的cache都会被失效,无法再继续局部自旋,调用get()方法都会占用bus,出现串行。当get()方法返回false后,此时,可能就会有多个处理器正试图同时执行getAndSet(true)方法,又导致了和TAS一样的情况出现。这一轮过后,各处理器又会在各自的cache中局部自旋,直到锁再次被释放。

       现在的问题焦点是:只要执行了一次无效的getAndSet(true)方法,都会导致所有处理器的cache被失效,失效后get()方法的调用就会占用bus,有可能导致处理器释放锁被延迟。如果能降低无效的调用次数,则能提升TTAS的速度。假设在第一轮中,有3个处理器同时执行getAndSet(true)方法,那么只有一个处理器能够获取到锁,另外的两个处理器又会重新恢复到TTAS的局部自旋中。当第一个处理器将锁释放后,这两个处理器又可能会同时执行getAndSet(true)方法,那么不可避免的又会存在一个处理器执行一次无效的getAndSet(true)方法调用。如果每个处理器在每次执行无效的getAndSet(true)方法调用后,都休眠一段随机的时间,再进入到TTAS的局部自旋中,则多个处理器同时执行getAndSet(true)方法调用的概率就能被降低。

  • 大小: 87.1 KB
  • 大小: 40 KB
分享到:
评论
1 楼 prizefrigh 2016-04-22  
TAS的while中,应该用compareAndSet吧?

相关推荐

    golang实现Redis分布式自旋锁+本地自旋锁

    在Golang中,实现分布式自旋锁和本地自旋锁是一种常见的并发控制策略,用于解决多线程或分布式系统中的资源竞争问题。...然而,实际应用中需要根据具体场景选择合适的锁类型,并考虑性能、容错性和可扩展性等因素。

    DirectShow采集音频,采用环形缓冲区对象将音频数据提供出来,还提供了自旋锁的概念

    在DirectShow的音频采集场景中,多个线程可能会同时尝试访问环形缓冲区,这时就需要自旋锁来确保数据的一致性和完整性。当一个线程获取到锁时,其他试图获取锁的线程将进入“自旋”状态,不断地检查锁是否释放,一旦...

    原子操作、信号量、读写信号量和自旋锁的API

    在现代操作系统中,特别是在多处理器环境下,确保数据的一致性和完整性至关重要。为此,Linux内核提供了多种同步机制来保护共享资源免受并发访问的影响。本文将详细介绍其中四种重要的同步机制——原子操作、信号量...

    一文读懂原子操作、内存屏障、锁(偏向锁、轻量级锁、重量级锁、自旋锁)、Disruptor、Go Context之上半部分.doc

    锁机制可以保证在多线程环境下,数据的安全和一致性。 五、Disruptor 框架 Disruptor 框架是一个高性能的并发框架,使用原子操作和锁机制来实现高效的并发处理。Disruptor 框架使用 RingBuffer 数据结构来实现高效...

    操作系统实验--读写锁

    在计算机系统中,特别是当多个线程需要同时访问同一数据时,读写锁能够提供一种策略,允许多个线程同时读取资源,但只允许一个线程写入资源,以确保数据的一致性和完整性。 读写锁通常由两部分组成:读锁和写锁。读...

    Linux内核基于对称多处理机的实现分析.pdf

    Linux内核通过缓存一致性协议(如MESI协议)和特定的指令来维护这一一致性。 **Linux内核的SMP优化** Linux内核为了优化SMP性能,还实现了诸如CPU亲和性(CPU affinity)的功能,允许程序指定其运行在哪个处理器上...

    Java并发体系.pdf

    JMM规定了线程如何访问和修改共享数据,以确保多线程环境下的正确性和一致性。为了优化程序性能,处理器和编译器可能会进行重排序,但这可能导致数据在多线程环境中的不安全性。JMM提供了一种叫做“顺序一致性”的...

    JavaJVM线程调优.pdf

    缓存行填充是为了解决CPU缓存一致性问题,如False Sharing。某些处理器有固定的缓存行大小,当多个线程共享同一缓存行中的变量时,可能会引起不必要的通信开销。Java中的`@Contended`注解可以标记字段以进行填充,...

    对称多处理(SMP)的论文资料

    这一技术的实现依赖于高效的进程调度、锁机制和缓存一致性协议。 1. **进程调度**:在SMP系统中,操作系统需要一个公平且高效的调度算法,将任务分配给不同的处理器。常见的调度策略有轮转法、优先级调度和基于权值...

    Lock free 论文集合,若干无锁数据结构实现的经典论文,500多页.zip

    性能分析包括了竞争率、缓存一致性开销、CPU分支预测等。优化策略可能包括减少原子操作的使用、利用缓存亲和性和预判性。 8. **内存对齐与数据布局**:无锁编程中,内存对齐和数据布局对于性能至关重要,因为不正确...

    Linux2.6内核对SMP系统支持的研究.pdf

    综上所述,Linux 2.6内核对SMP系统的支持涉及了进程调度、同步机制和负载平衡等多个方面,这些优化措施确保了多处理器环境下的高效运行和资源利用率,同时也为实时性和系统稳定性提供了保障。这些技术对于系统开发者...

    Lec08_索引并发1

    为了确保数据的一致性和正确性,系统必须采用并发控制策略,这就是所谓的“Physical Correctness”。这一概念确保在并发访问时,索引内部的数据结构保持稳定。 在并发控制中,Latch是一种常用机制,它与Lock有所...

    perfboot-eb.2021.12.22a.pdf

    - **缓存一致性协议**:介绍如MESI、MOESI等缓存一致性协议,解释它们如何确保多核环境下的数据一致性。 - **锁的实现与优化**:深入讲解互斥锁、读写锁、自旋锁等不同类型的锁,以及如何通过锁细化、锁消除等手段...

    CVI 03.多线程数据保护(线程锁)

    线程锁,也称为互斥锁,是实现线程安全的一种机制,用于确保在任何时刻只有一个线程能够访问特定的共享资源,以防止数据的不一致性和错误。本节我们将深入探讨“CVI 03.多线程数据保护(线程锁)”这一主题,了解其...

    通常是使用硬件提供的有关同步指令PPT学习教案.pptx

    5. **一致性实现锁**:在没有缓存一致性机制的系统中,可以通过存储器中的原子操作请求加锁和解锁。而在有缓存一致性的机器中,锁可以被缓存并在一致性协议下保持一致。这种方法允许进程在本地Cache中操作锁,提高了...

    高并发秒杀案例

    在构建高并发秒杀系统的过程中,我们面临的主要挑战是如何在短时间内处理大量的用户请求,确保系统的稳定性和用户体验。这里,我们将围绕“高并发秒杀案例”这一主题,结合使用SpringMVC和Mybatis两大技术框架,深入...

    Lockfree论文集合,若干无锁数据结构实现的经典论文,500多页.zip

    4. 数据结构的无锁实现:如无锁队列、栈、哈希表等,这些数据结构在多线程环境下如何通过原子操作保持一致性。例如,无锁队列通常使用双端队列(deque)结构,通过CAS操作在头部和尾部进行插入和删除。 5. 性能分析...

    深入理解linux内核中文第三版-第五章

    4. **读写屏障(Memory Barriers)**:在多处理器系统中,由于缓存一致性问题,有时需要插入内存屏障来确保指令的执行顺序。这在处理内核同步时非常关键,可以防止数据的错误排序。 5. **读写复制(Copy-On-Write, ...

    Java线程与缓存面试题,面试必看,附代码.docx

    读写锁允许多个读取线程并发,但写入操作独占,保证数据一致性。 面试中,理解并能灵活运用这些知识点至关重要,尤其是在阿里巴巴这样的大公司,对并发编程和高效缓存的掌握是衡量一个Java开发者能力的重要标准。...

Global site tag (gtag.js) - Google Analytics