java并发编程实战之原子变量与非阻塞算法

版权声明:转载注明出处 https://blog.csdn.net/nobody_1/article/details/82851485

前面讲到通过内置锁或者显示锁保证多线程的安全,虽然很方便,但也存在缺点:
1. 当在锁上发生竞争时,挂起和恢复线程都存在很大的开销,比如上下文切换和线程调度;
2. volatile修饰的变量虽然不存在上述开销,但其不能保证原子操作的缺陷限制了它的使用;
3. 获取锁的线程若不能安全的执行结束或者正确的释放锁,则存在死活、活锁等严重问题,并导致等待锁的线程无法执行;

享受锁带来便利的同时,也无法避免锁带来的性能开销。早在此系列的第一篇博文中指出:多线程的出现是因为摩尔定理的失效,把本应通过硬件提升系统性能转变到通过软件提升系统性能;随着时代的发展,硬件层面又有了一定的突破。

硬件对并发的支持

在针对多处理器操作而设计的处理器中提供了一些特殊指令,用于管理对共享数据的并发访问。在现代处理器中包含了某种形式的原子读-改-写指令,例如比较并交换(compareAndSwap)或者关联加载/条件存储(Load-Linked/Store-Conditional),操作系统和JVM使用这些指令来实现锁和并发的数据结构。比较并交换(CAS)指令是大多数处理器架构中采用的方法:
CAS包含三个操作数:需要读写的内存位置V进行比较的值A拟写入的新值B;当且仅当V的值等于A时,CAS才会通过原子方式用新值B来更新V的值,否则不会执行任何操作,另外无论是否执行成功,CAS操作都会返回V值。下面代码模拟CAS操作的实现:

public class SimulateCAS {

	private int value;
	
	public synchronized int get(){
		return value;
	}
	
	public synchronized int compareAndSwap(int exceptValue, int updateValue){
		int oldValue = value;
		if (oldValue == exceptValue) {
			value = updateValue;
		}
		return oldValue;
	}
	
	public synchronized boolean compareAndSet(int exceptValue, int updateValue){
		return (exceptValue == compareAndSwap(exceptValue, updateValue));
	}
}

那么CAS是如何处理竞争的呢?

当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其他线程都将失败。但失败的线程并不会被挂起(这点与锁不同),而是被告知在这次竞争中失败,并可以再次重试或者不执行任何操作,这种处理机制可减少与锁相关的活跃性风险。

JVM是如何支持CAS指令?

Java在5.0之前需要编写明确的代码,否则无法执行CAS操作;java在5.0版本中引入底层的支持,在int/long/对象的引用等类型上都公开了CAS操作,并且JVM把它们编译为底层硬件提供的最有效方法。在支持CAS平台上,运行时把它们编译为相应的机器指令。在原子变量类中使用了这些底层的JVM支持为数字类型和引用类型提供一种高效的CAS操作,另外在JUC中的大多数类在实现时则直接或者间接地使用了这些原子操作类。

原子变量类

特点:将发生竞争的范围缩小到单个变量上,因此原子变量类粒度更细、量级更轻不需要挂起或者重新调用线程,因此线程在执行时不易发生延迟,并且发生竞争也容易恢复过来;原子变量类主要有以下四类:
标量类: AtomicInteger/AtomicLong/AtomicBoolean/AtomicReference
更新器类
数组类: Integer/Long/Reference类型的数组
符合变量类
可以通过查看AtomicInteger类的源码理解其实现原理:

public AtomicInteger(int initialValue) {
        value = initialValue;
}

public AtomicInteger() {}

AtomicInteger类内部维持一个volatile关键字修饰的int类型的value,用于记录AtomicInteger变量的值,在初始化时可确定value的值;另外,通过final类型的get和set方法获取和注入value的值;

public final int get() {
    return value;
}

public final void set(int newValue) {
    value = newValue;
}

注意,还有另外一个set方法注入value的值:

public final void lazySet(int newValue) {
    unsafe.putOrderedInt(this, valueOffset, newValue);
}

lazySet相比Set方法,lazySet方法更注重竞争性,在易发生写冲突的情况下建议使用lazySet方法。
AtomicInteger类内部的其他方法:

public final int getAndSet(int newValue) {
    return unsafe.getAndSetInt(this, valueOffset, newValue);
}
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}
public final int getAndDecrement() {
    return unsafe.getAndAddInt(this, valueOffset, -1);
}
public final int getAndAdd(int delta) {
   return unsafe.getAndAddInt(this, valueOffset, delta);
}
public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
public final int decrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
}
public final int addAndGet(int delta) {
    return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}

可见方法都是调用unsafe对象的方法,最终被编译成机器指令,实现原子类操作。

锁与原子变量的性能比较?

毫无疑问,原子变量的性能将超过锁的性能,这是因为锁在发生竞争时会挂起线程,从而降低了cpu的使用率和共享内存总线上的同步通信量。另外,如果使用原子变量,竞争将由发出调用的类负责。我们知道,原子变量的失败机制是不断重试,但这种重试容易造成更激烈的竞争。要想消除竞争,就需要避免对共享资源的访问,就有了后来的ThreadLocal类,这个类可以为每个线程提供一个变量的副本,并保持每个线程中副本的一致性和可见性,它所提供的性能高于锁和原子变量。

CAS实现非阻塞算法

如果在某种算法中,一个线程的失败或挂起不会导致其他线程也失败或者挂起,那么这种算法被称为非阻塞算法;如果在算法的每个步骤中都存在某个线程能够执行下去,那么这种算法被称为无锁算法;结合CAS对竞争的处理及其特性可知,利用CAS原理来调度线程而实现的算法既是非阻塞算法,也是无锁算法。常用的栈,队列,优先队列以及散列表等都是非阻塞算法。(在插入或者移除节点的过程中,利用CAS原理保证操作的原子性和可见性)

参考资料

《java并发编程实战》

总结

最近一两个月都在学习并发编程相关的知识,对于《java并发编程实战》这本书大体看了两遍,第一遍阅读+编码,第二遍总结博客;由于工作的原因,博客的思考深度可能还不够,仅仅是为了能够加深自己的理解和总结,后面再看就会有一个大概的记忆。
高并发在日常开发过程中其实并不常用,究其原因是高并发会发生不可预见的错误,会严重影响系统的稳定性,且对于小网站无需使用高并发,因此主线程就能完成的任务无需徒增高并发带来的风险。
再来说说这本说,个人感觉相比《Java多线程编程核心技术(完整版)》、《实战Java高并发程序设计》,《java并发编程实战》确实是一本难得的好书,但我建议先看前面两本书,再看这本书对于理解书中的内容有很大的益处。前面两本大概讲的是如何使用,而这本书则讲的为什么会这样使用,是如何设计的,采用了什么机制等;抱着“知其然,知其所以然”的态度来拥抱技术吧。

猜你喜欢

转载自blog.csdn.net/nobody_1/article/details/82851485