无锁算法 - CAS
CAS(比较与交换,Compare and swap) 是一种有名的无锁算法。
- CAS是乐观锁的核心,它是一个不断尝试的机制,不用锁任何资源。
- CAS的队列都是无界;有界队列应该会加锁处理边界问题。
内存值V,旧的预期值A,要修改的新值B。
当且仅当 【预期值A = 内存值V】时,将内存值V修改为B,否则什么都不做。
Unsafe是CAS的核心类,Java无法直接访问底层操作系统,而是通过本地(native)方法来访问
JDK内置队列 - 速览
从 数据结构 来看,来保证线程安全的方式
- 基于 数组 线程安全的队列
- (ArrayBlockingQueue),加锁
- 基于 链表 线程安全队列
- LinkedBlockingQueue 加锁
- ConcurrentLinkedQueue 原子变量的CAS 不加锁(若存在伪共享问题,推荐用Disruptor)
- 基于 堆 线程安全队列 (加锁)
- DelayQueue 延迟队列
- PriorityBlockingQueue 优先级队列
但是对 volatile类型的变量进行 CAS 操作,存在伪共享问题。 花开两朵,各表一枝(逃
加锁队列/有界队列
ArrayBlockingQueue
伪共享
当生产者线程put一个元素到ArrayBlockingQueue时, putIndex会修改, 从而导致消费者线程的缓存中的缓存行无效, 需要从主存中重新读取。这种无法充分使用缓存行特性的现象, 称为伪共享。
使用数组来维护VolatileLong,故它们在内存中是连续存储的。
由于该类内的value成员已经使用volatile关键字来修饰,故CPU要保证它的修改对所有线程都立即可见。
value的自增值会直接回写到内存,并将对应的缓存行置为失效
缓存行
以缓存行(cache line)为单位存储的(2^N幂个连续字节),最常见的缓存行大小是64个字节。
多线程修改 【同一缓存行 的变量】,会无意中影响彼此的性能
@Contended
Disruptor以及@Contended注解
Java 8用@Contended在【类/字段】上的注释,来进行【缓存行】填充,解决多线程下的伪共享冲突。
-XX:-RestrictContended 添加这个参数才能够开启Contented
@Contended("group1")
private Object field1;
@Contended("group1")
private Object field2;
@Contended("group2")
private Object field3;
无界队列
Disruptor
设计方案
(1)环形数组结构
为了避免垃圾回收,采用数组而非链表。
数组遍历更快(CPU的多级缓存机制实现原理,PageCache缓存数据的邻近数据)
(2)元素位置定位
数组长度2^n,通过位运算,加快定位的速度
下标采取递增的形式(不用担心index溢出,long类型,100万QPS也需30万年)
(3)无锁设计
每个 生产者/消费者 线程,先申请可以操作的元素的index
伪内存共享
class LhsPadding {
protected long p1, p2, p3, p4, p5, p6, p7;
}
class Value extends LhsPadding {
protected volatile long value; // 核心 + 前后15个中的7个无关的long >= 64B
}
class RhsPadding extends Value {
protected long p9, p10, p11, p12, p13, p14, p15;
}
public class 【Sequence】 extends RhsPadding {
static final long INITIAL_VALUE = -1L;
private static final Unsafe UNSAFE;
private static final long VALUE_OFFSET;
static {
UNSAFE = Util.getUnsafe();
try {
VALUE_OFFSET = UNSAFE.objectFieldOffset(Value.class.getDeclaredField("value"));
} catch(final Exception e) {
throw new RuntimeException(e);
}
}
}
无锁队列实现
单生产者:比较简单
多生产者:
每个线程获取不同的一段数组空间进行操作(在分配元素的时,通过CAS判断这段空间是否已被分配)
多生产者下,引入一个与Ring Buffer大小相同的buffer:available Buffer(替身)
当某个位置写入成功的时候,便把availble Buffer相应的位置置位,标记为写入成功。
读取时,会遍历available Buffer,来判断元素是否已经就绪。
等待策略
生产者的等待策略
暂时只有休眠1ns。
LockSupport.parkNanos(1);
消费者的等待策略
(1)BlockingWaitStrategy(默认),是效率最低的等待策略,但也是CPU使用率最低和最稳定的选项
(2)BusySpinWaitStrategy,是性能最高的等待策略,同时也对部署环境要求最高
(3)YieldingWaitStrategy,需要高性能而且事件消费者线程比逻辑内核少
(4)SleepingWaitStrategy,不需要低延迟,且事件影响比较小。比如异步日志功能。
生产消费模式
并发系统中提高性能最好的方式之一就是单一写者原则
生产者
Disruptor<LongEvent> disruptor = new Disruptor(factory,
bufferSize,
ProducerType.SINGLE, // 单一写者模式,
executor
);
消费者
(1)串行消费
public static void serial(Disruptor<LongEvent> disruptor){
disruptor.handleEventsWith(new C1_EventHandler()).then(new C2_EventHandler());
disruptor.start();
}
(2)菱形执行
public static void diamond(Disruptor<LongEvent> disruptor){
disruptor.handleEventsWith(new C1_EventHandler(),new C2_EventHandler())
.then(new C3_EventHandler());
disruptor.start();
}
(3)相互隔离
public static void parallelWithPool(Disruptor<LongEvent> disruptor){
disruptor.handleEventsWithWorkerPool(new C11EventHandler(),new C11EventHandler());
disruptor.handleEventsWithWorkerPool(new C21EventHandler(),new C21EventHandler());
disruptor.start();
}
(3)航道模式
/**
* 串行依次执行,同时C11,C21分别有2个实例
*
* p --> c11 --> c21
* @param disruptor
*/
public static void serialWithPool(Disruptor<LongEvent> disruptor){
disruptor.handleEventsWithWorkerPool(new C11EventHandler(), new C11EventHandler())
.then(new C21EventHandler(), new C21EventHandler());
disruptor.start();
}