无锁简介
无锁分为两大派系:
-
乐观派系:它们认为事情总会往好的方向去发展,总是认为坏的情况发生概率特别小,可以无所顾忌的做任何事情.
-
悲观派系:它们总会认为发展事态如果不及时控制,以后就无法挽回,即时此种局面不会发生的情况下。
上述两大派系映射到并发编程中就如同加锁与无锁策略,即加锁是一种悲观策略,无锁是一种乐观策略,因为对于加锁的并发程序来说,它们总是认为每次访问共享资源时总会发生冲突,因此必须对每一次数据操作实施加锁策略。
而无锁则总是假设对共享资源的访问没有冲突,线程可以不停执行,无需加锁,无需等待,一旦发现冲突,无锁策略则采用一种称为CAS的技术来保证线程执行的安全性,这项CAS技术就是无锁策略实现的关键。
串行无锁
无锁串行最简单的实现方式可能就是单线程模型了,Redis/Nginx都采用了这种方式。
Redis是单线程仅指Redis服务端的网络IO是单线程的,Redis的集群数据同步,持久化等都是多线程的。
Redis 采用网络IO多路复用技术,来保证在多连接的时候系统的高吞吐量。
多路: 指的是多个socket网络连接;
复用: 指的是复用一个线程。多路复用主要有三种技术:select,poll,epoll。epoll是最新的, 也是目前最好的多路复用技术。
Redis使用的是非阻塞IO、IO多路复用,使用了单线程来轮询描述符,将数据库的开、关、读、写都转换成了事件,减少了线程切换时上下文的切换和竞争。
结构无锁
利用硬件支持的原子操作实现无锁的数据结构,很多语言都提供CAS原子操作,可以用于实现无锁队列。
什么是CAS
CAS全称为Compare And Swap即比较并交换。
CAS有三个操作数,旧值A,新值B,以及需要读取的内存值V,在更新一个变量时,当且仅当A=V相同时,CAS才会将内存值V修改为B,否则什么都不做。
Java里的CAS是通过调用Unsafe类的native方法,再由C调用CPU底层命令实现的。
原子包 java.util.concurrent.atomic(锁自旋)。
前面我们说到CAS操作的原子性是通过硬件来保证,这里作进一步的解释说明。CPU层面上,CAS的比较-写入操作上是通过cmpxchg指令去实现完成的。然而不幸的是,cmpxchg指令并不是一个原子操作。
即可能会发生这样的场景,线程A在执行cmpxchg指令的过程中,发现当前内存值V与预期值E一致,正准备将新值U写入内存,这个时候另外一个线程B打断了线程A的操作,将该变量修改了。显然这个变量发生了线程安全的问题,为此为了保证cmpxchg指令的原子性,不会被打断,需要在cmpxchg指令前添加一个前缀指令lock。通过对cmpxchg指令进行加锁(总线锁或缓存锁)来保证操作的原子性。通常我们会说CAS算法是一个无锁算法,但其实我们可以看到底层依然是加了锁的,只不过这个锁的粒度是很小的。
CAS的优缺点
缺点:
- CPU开销比较大:虽然CAS算法是非阻塞的,但是如果CAS操作一直不成功不断循环自旋,将会大大浪费CPU资源
- 只能保证一个共享变量的原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。
- ABA问题,可以通过加版本号解决
优点:一般情况下性能优于锁的使用。
图解Redis单线程模型
乐观锁:CAS 算法
不可不说的Java“锁”事