网站 建设设计,网站空间ip是一个域名,wordpress建立相册,百度题库转自http://blog.csdn.net/u011277203/article/details/9223545在并发环境中#xff0c;对于共享资源通常会采用显式的锁机制(比如synchronized或ReentrantLock)来保证在任意时刻只会有一条线程访问这些变量#xff0c;并且这些变量的修改对随后获取锁的线程是可见的。无法获…转自http://blog.csdn.net/u011277203/article/details/9223545在并发环境中对于共享资源通常会采用显式的锁机制(比如synchronized或ReentrantLock)来保证在任意时刻只会有一条线程访问这些变量并且这些变量的修改对随后获取锁的线程是可见的。无法获取锁的线程会进入阻塞状态并被JVM和操作系统挂起在未来某一时刻被调度重新获取锁挂起和恢复线程会产生很多的系统消耗和较长时间的中断。线程的切换同时会引起上下文切换即把当前线程的运行时上下文保存起来装入新线程的运行时上下文所以上下文切换并不是免费的另外被换入的新线程所需要的数据不太可能在CPU Cache中因此上下文切换会导致Cache Missing激增新线程开始执行时性能会相对较低。在高并发竞争非常激烈的场景下显式锁的开销会非常大将严重影响系统的性能。所以在一些场景下使用非阻塞方案解决并发问题会显著提升系统性能。VolatileVolatile变量是一种比锁更轻量的同步机制因为它不会产生上下文切换和线程调度。Volatile变量可以保证可见性即变量被一条线程修改后其他线程都会读取到最新值不过volatile变量也有自身的限制它不支持原子操作当变量的更新依赖其他变量或自身时(比如i)volatile不能保证结果的正确性。最近在项目中需要封装Memcache客户端实现双机热备和自动切换。这里设置一个变量currentCluster来表示当前使用的是主集群还是备用集群业务请求根据此变量来判断优先访问的集群同时有1条守护线程不断轮询2个集群一旦发现有机器不可用立刻切换即修改变量currentCluster的值。所以currentCluster变量会被多条线程读写且访问非常频繁不过currentCluster变量的修改并没有依赖其他变量或自身只需要保证可见性即可适用与volatile变量的应用场景由于主存的访问速度相对于CPU的处理速度比较慢所以CPU通过Cache降低内存延时的成本编译器和CPU本身会对指令做一些优化改变指令的执行顺序在不改变最终结果的前提下提高CPU Cache的命中率和CPU流水线的执行效率此过程称为指令重排。当数据不可变或者限制在同一条线程范围内CPU Cache和指令重排是无害的但是如果在多核处理器并发访问共享可变状态的场景下不同的Cache缓存的数据可能会不一致共享可变状态的内存操作被重新排序这些优化会造成程序行为不定造成共享变量的不可见性。在Java程序中加入volatile关键字可有效解决这些问题。在C语言中对volatile关键字修饰的共享变量执行写操作的引发2件事情1. 将当前处理器缓存行的数据写回到系统内存2. 这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效JVM增强了Java中volatile关键字的语义在访问变量时会加入内存屏障使得前后指令不会被重排。因此Java中的volatile关键字可以保证可见性即共享变量被修改后其他线程立刻可以读取到最新值。由于volatile变量在被修改时会将CPUCache中的数据失效掉而CPU Cache的最小执行单位是Cache Line所以包含volatile变量的整条Cache Line的数据都会失效。这里需要注意”伪共享”问题如果volatile变量长度不超过Cache Line在相邻变量之间需要padding否则会产生大量Cache Missing。这里对于CPU的细节并不展开讨论感兴趣的同学可以阅读振辉在4月Rigel技术月刊中发表的文章《优化到 CPU ––java 与 CPU 缓存》原子操作上一小节中提到volatile关键字只能保证可见性如果变量的修改依赖其他变量或自身则volatile无能为力此时需要实现原子性即所有操作是不可分割的不会被其他线程打断。在现代多核处理器中都会提供原子指令比如CAS(compare and swap)该指令有3个操作数需要操作的内存地址V预期的原有值oldValue要写入的新值newValue。使用CAS指令执行更新操作时如果V上的值和oldValue相同则原子的V上的值更新为newValue如果在当前线程读取oldValue之后其他线程执行了更新操作则当前线程的CAS指令返回失败。当多条线程同时试图使用CAS指令更新同一个共享变量时会有一条线程成功更新变量而其他线程会失败。由于这里应没有涉及锁的操作所以失败线程并不会被挂起也不会阻塞他们只是被告知这次更新操作失败可以重试或做其他的事情。下面的一段示例代码是一个计数器的非阻塞实现在increment的过程中不断使用CAS操作更新变量直到成功为止java.util.concurrent包中的原子实现也是采用类似的机制。利用CAS指令除了能够实现简单数据类型的原子操作(比如java.util.concurrent包中的AtomicIntegerAtomicLong)外还能实现复杂数据类型的原子操作。实现复杂数据类型的非阻塞算法的关键在于如何在维护数据一致性时将原子更新的范围限定在一个简单变量上。比如一个栈每个元素Node(value, next)只会指向一个其他的元素并且每个元素也只会被一个其他的元素指向。对于push方法会创建一个新节点指向栈顶元素top并使用java.util.concurrent.atomic.AtomicReference 的CAS操作尝试替换top元素如果top没有被其他线程修改则替换成功否则重新获取top元素再次尝试替换知道成功为止。自旋锁当程序中需要保证多个资源或变量的一致性无法将更新范围限定在一个变量上时必须使用显式的锁机制比如synchronized关键字或ReentrantLock但如上文所述由于会产生阻塞这种显式锁机制的开销比较大尤其是在高并发场景下。这里介绍一种非阻塞的显式锁机制——自旋锁。自旋锁采用java.util.concurret包提供的AtomicBoolean类表示锁的状态false表示没有其他线程获取当前锁true表示当前锁已被其他线程获取。当有多条线程同时访问lock()方法试图获取锁时只有一条线程可以成功其他线程会停留在while(state.get()){}循环中只有当活动线程调用unlock()方法释放锁时才会有另一条线程跳出while(state.get()){}循环因为unlock方法将state设置为false。由于这里没有获取到锁的线程并没有被阻塞所以不会有阻塞相关的开销。一条线程成功获取锁后所有的非活动线程都会不停的循环竞争会非常激烈造成CPU资源的浪费。所以可以引入让步锁的机制降低CPU的开销。当线程获取锁失败时会调用backoff()方法时sleep一段时间避免多条线程同时循环并且每条线程恢复的时间不一样减少了竞争降低了CPU的开销。这里让步锁的sleep时长设定非常关键如果太短效果不明显如果太长会降低系统的吞吐量。根据同步块的预计运行时长来设置比较合理。总结在实际应用场景中为避免阻塞带来的开销使用非阻塞方案解决并发问题是非常有必要的。当更新范围可以限定在一个变量上时可以使用volatile关键字或原子操作。如果需要保证多个资源或变量的一致性则可以考虑自旋锁不过对于同步块执行时间较长或执行时间长度差距较大的场景并不适合使用自旋锁因为很难避免CPU的过度开销所以这种场景下不妨直接使用synchronized关键字或ReentrantLock。事实上Synchronized关键字和ReentrantLock都在不同程度上实现了自旋锁在竞争开始时会先尝试自旋如果能够获取锁则直接返回否则进入阻塞状态。不过这里的自旋时长并不可控如果已经确定同步块会执行比较快(一般来说没有IO和复杂计算)直接使用自旋锁效果会更佳。关于synchronized关键字和ReentrantLock的内部实现原理后面会专门写文章详细讨论。