什么是锁呢?
我搜集到的资料里表明,锁是一个内存中的···值?
如果这个值是0,就代表没人加锁。如果值是1,就代表有人加锁。
但想要正确地实现锁(使用锁?),需要注意两方面问题。
一方面是原子操作,另一方面是内存重排序。
首先是原子操作问题。
把内存中的一个值变成1或者0,设计多次操作。在x86平台上,大概有读取数据到寄存器、修改、写寄存器的值到内存。这是不原子的。
x86是缓存一致的,保证(只保证)操作一个缓存行内的数据是原子的。
其次是内存重排序问题。
内存重排序的基本原则是,单线程情况下程序必须看不出来做过重排序。
但在多线程下,就不能保证了。这需要借助内存模型。Java肯定是有的。
所以,想要正确的锁,我们需要解决内存重排序和原子操作问题。lock前缀+指令(锁内存总线)可以正确地实现原子CAS。
自旋锁的问题
太浪费cpu了。
有的时候我们需要让线程挂起。这类操作其实有对应的系统调用------futex。
futex就是那种,拿到锁就进入临界区,拿不到就挂起的机制。
Java等语言会在用户态实现一个futex的变体,如AQS队列。它们可能是混血的,尝试CAS几次,如果失败了,再futex(需要变态,比较重,因此我们尽量不futex)。这种机制很聪明。
这种思想不光可以用在线程上,还可以用在routine上。因为挂起线程的操作很重,所以我们有了混血futex。但对于routine,让他在用户态睡眠是ok的。这不需要变态。事实上golang的信号量就是这么实现的。
但是golang的semaphore问题可能会导致协程频繁的唤醒、抢锁失败、挂起。这会导致大量的协程做没用事情,甚至饿死。
为了解决这个问题,golang实现了mutex。mutex既有spin,又有semaphore,还有另一层实现。