1 常见的锁策略
1.1 乐观锁vs悲观锁
悲观乐观是对后续锁冲突是否频繁做出的预测。
如果预测接下来发生锁冲突的概率不大,就可以少做一些工作,就称为乐观锁;
如果预测接下来发生锁冲突的概率较大,就需要多做一些工作,就称为悲观锁。
1.2 重量级锁vs轻量级锁
重量级锁,锁的开销较大;轻量级锁,锁的开销较小。
通常,乐观锁就是轻量级锁,悲观锁就是重量级锁。
1.3 自旋锁vs挂起等待锁
自旋锁是轻量级锁的的一种典型实现,往往在用户态进行,例如使用一个while循环,不断判断当前锁是否被释放,如果没释放,就继续循环,如果已经释放,就获取到锁并结束循环。(忙等,消耗cpu但是换来了更快的响应速度);
挂起等待锁要借助系统api来实现,一旦出现锁竞争,就会在系统内核中执行一系列的操作(比如让这个线程进入阻塞状态,不参与cpu的调度,而阻塞的开销通常是很大的)。
1.4 读锁vs写锁
读锁:读的时候别的线程可以读,但是不可写。
写锁:写的时候别的线程不可以读,也不可以写。
1.5 公平锁vs非公平锁
当一堆线程等待锁释放想要拿到锁的时候,该按照什么策略拿呢?
公平锁:先来的线程先拿到锁。(先来后到)
非公平锁:每个线程以相同的概率竞争锁。
以上策略都是锁的一些特点,那前面说过的synchronized属于哪种锁呢?
1、对于"悲观乐观"是自适应的。
2、对于"重量轻量"是自适应的。
3、对于"自旋挂起"是自适应的。
4、不是读写锁。
5、是可冲入锁。
6、是非公平锁。
对于初始情况,synchronized会预测锁冲突的概率不大,此时以乐观锁的模式来运行(也就是轻量并且自旋的);如果后续锁冲突的概率加大,那么此时就会自适应的变为悲观锁(也就是重量挂起的)。
2 CAS
CAS就是compare和swap,也就是比较并且交换,那它比较的是什么呢?是内存和寄存器中的值。
CAS(M,A,B):比较M和A的值,如果相同,就把M和B的值交换,返回true;如果不同,就什么都不做,直接返回false。
这是CAS的伪代码:
CAS本质上是cpu提供的一个指令,是具有原子性的,之后又被操作系统封装,提供api,又被JVM封装,也提供api,最后供程序员使用。
既然CAS是具有原子性的,那么它也可以解决"线程安全"问题,从而在一些场景中代替加锁策略,基于CAS实现"线程安全"的编程称为无锁编程。这样做的好处是什么呢:不仅可以保证线程安全,同时比加锁的效率更高;但是,也存在一些缺点:1、代码会更复杂,不好理解 2、仅使用于一些场景,不如加锁普适性强。
2.1 CAS的关键问题:ABA问题
什么是ABA呢?当一个线程将数据A取出,经过一系列操作后把它变为B,但是最后又通过一系列操作将它变回A,那么对于另一个执行CAS操作的线程来说,可能会认为这个数据是没发生过变化的(虽然有的时候也不会出现bug),这种问题就是ABA问题。
此时,左侧的CAS中value的值不等于oldValue的值,就不执行任何操作,直接返回false。但是如果发生ABA问题,例如此时恰好有个人又给我转了500元,那么我的value又变回1000,等于oldValue,再次扣款,这就出现了很严重的bug,本来我只打算取500,但是现在取了1000!!
那又该怎么解决这个问题呢?
很简单,只需要让判定的数值按照一个方向变化即可(不要出现反复横跳,一会加一会减就可能会出现ABA问题)。在上述这个例子中,我们只需要将CAS的判定条件改为版本号,初识版本号为v1,每执行一次操作,版本号就会加1,那么,只要有其它线程穿插执行,版本号一定不等于old版本号,此时一定不会执行交换操作,而是会直接返回false,bug顺利解决。
3 信号量Semaphore
信号量是操作系统中比较重要的概念,其实信号量就是一个计数器,描述了"可用资源"的个数,每次申请一个可用资源,就要让信号量减1(P操作),每次释放一个操作,就要让信号量加1(V操作),那如果信号量已经为0的情况下继续申请资源(使用P操作)会发生什么呢?此时就会阻塞等待,直到其它线程释放一个资源(使用V操作),才能继续往下执行。
前面提到过的锁其实就是一种特殊的信号量,锁就是可用资源为1的信号量,所以一旦加锁,其它线程就会发生阻塞,因为加锁相当于使信号量减1,而此时信号量一共就是1,所以直到该线程释放锁,也就相当于执行V操作,其它线程才能获得锁