CAS
CAS,自旋锁跟compare and set有关系,但是并不是CAS就是自旋锁。
我们看一段代码:
/* 不同线程检测最大值 */
AtomicLong largest = new AtomicLong();
long obsvValue = 0;
/* 错误的方式,此更新不是原子性的 */
largest.set(Math.max(largest.get(), obsvValue));
/* 正确的方式,这种方式比锁快 */
long oldValue;
long newValue;
do {
oldValue = largest.get();
newValue = Math.max(obsvValue, oldValue);
} while (!largest.compareAndSet(oldValue, newValue));
compareAndSet内部
public final boolean compareAndSet(long expect, long update) {
return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
}
有好几个AtomicLong 方法用到了cas:
/* @since 1.8 */
public final long updateAndGet(LongUnaryOperator updateFunction) {
long prev, next;
do {
prev = get();
next = updateFunction.applyAsLong(prev);
} while (!compareAndSet(prev, next));
return next;
}
/* @since 1.8 */
public final long accumulateAndGet(long x,
LongBinaryOperator accumulatorFunction) {
long prev, next;
do {
prev = get();
next = accumulatorFunction.applyAsLong(prev, x);
} while (!compareAndSet(prev, next));
return next;
}
/*java 1.7*/
public final long getAndIncrement() {
while (true) {
long current = get();
long next = current + 1;
if (compareAndSet(current, next))
return current;
}
}
/*java 1.8*/
public final long getAndIncrement() {
return unsafe.getAndAddLong(this, valueOffset, 1L);
}
再扩展一下java.util.concurrent.atomic.AtomicReference
// 创建两个String对象
String s1 = "a";
String s2 = "b";
// 新建AtomicReference对象,初始化它的值为s1对象
AtomicReference<String> ar = new AtomicReference<>(s1);
// 通过CAS设置ar。如果ar的值为s1的话,则将其设置为s2。
ar.compareAndSet(s1, s2);
String s3 = ar.get();
System.out.println("s3="+s3);
System.out.println("s3.equals(s1)="+s3.equals(s1));
System.out.println("s3.equals(s2)="+s3.equals(s2));
CAS的ABA问题
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B
AtomicLong largest = new AtomicLong();
long obsvValue = 0;
long oldValue;
long newValue;
do {
oldValue = largest.get();
newValue = Math.max(obsvValue, oldValue);
} while (!largest.compareAndSet(oldValue, newValue));
但是这个CAS也有局限性,如果A线程获取到预期值a,结果B线程正在将内存值从a改为c再改为a。则线程A没有感知到这个时间差的数据变化
解决办法:
使用 AtomicStampedReference
/*** ABA的问题*/
private static void testABA1() throws InterruptedException {
AtomicInteger atomicInt = new AtomicInteger(100);
new Thread(() -> {
atomicInt.compareAndSet(100, 101);
atomicInt.compareAndSet(101, 100);
}).start();
new Thread(() -> {
ThreadTool.sleep(1000);
boolean c3 = atomicInt.compareAndSet(100, 102);
System.out.println(c3); //true
}).start();
}
/**
* 不会出现ABA问题
*/
private static void testABA2() {
AtomicStampedReference<Integer> rf = new AtomicStampedReference<>(100, 0);
new Thread(() -> { ThreadTool.sleep(1000); rf.compareAndSet(100, 101, rf.getStamp(), rf.getStamp() + 1); rf.compareAndSet(101, 100, rf.getStamp(), rf.getStamp() + 1); }).start(); new Thread(() -> { int stamp = rf.getStamp(); System.out.println("before sleep : stamp = " + stamp); // stamp = 0 ThreadTool.sleep(2000); System.out.println("after sleep : stamp = " + rf.getStamp());//stamp = 1 boolean c3 = rf.compareAndSet(100, 101, stamp, stamp + 1); System.out.println(c3); //false }).start(); }
自旋锁的例子,自旋锁是否可重入的问题:
可重入锁,也叫做递归锁,指的是同一线程外层函数获得锁之后,内层递归函数仍然有获取该锁的代码,但不受影响。
在JAVA环境下 ReentrantLock 和synchronized 都是可重入锁,其最大的作用是避免死锁。
我们看一下自旋锁的例子:
很明显如果同一个线程两次lock会造成死锁。因为第一次拿到锁后,标志位置为非空,持锁,如果再效用lock()可死循环等待标志位为空,死锁了。
public class SpinLock
{
private AtomicReference<Thread> owner =new AtomicReference<>();
public void lock(){
Thread current = Thread.currentThread();
while(!owner.compareAndSet(null, current)){
//
}
}
public void unlock(){
Thread current = Thread.currentThread();
owner.compareAndSet(current, null);
}
}
让自旋锁可重入的方式:
public class SpinLock1
{
private AtomicReference<Thread> owner =new AtomicReference<>();
private int count =0;
public void lock(){
Thread current = Thread.currentThread();
if(current==owner.get())
{
count++;
return ;
}
while(!owner.compareAndSet(null, current)){
}
}
public void unlock (){
Thread current = Thread.currentThread();
if(current==owner.get()){
if(count!=0){
count--;
}else{
owner.compareAndSet(current, null);
}
}
}
}
自旋锁和互斥锁
自旋锁:
是一种非阻塞锁,如果某线程需要获取自旋锁,但该锁已经被其他线程占用时,该线程不会被挂起,而是在不断的消耗CPU试图获取锁。
死循环检测锁的标志位,任意时刻只有一个线程能够获得锁,其他线程忙等直到获得锁。
多处理器多线程中广泛使用,要求临界区执行短,且CPU资源不紧张,这样获取的锁可以尽快释放,如果临界区执行时间过长,会造成cup飙高。
优点是不会使线程发生切换,没有昂贵的系统调用,一直处于用户态,执行速度快。
缺点是消耗CPU。
互斥锁:
是阻塞锁,当某线程无法获取互斥量时,该线程会被直接挂起,该线程不再消耗CPU时间,当其他线程释放互斥量后,操作系统会激活那个被挂起的线程。
线程从加锁到解锁,过程中有上下文的切换,临界区的持锁时间并不会对互斥锁的开销造成影响。
适用于临界区持锁时间比较长的操作,比如临界区有IO,竞争激烈,或者是单核处理器。
优点是不会忙等,得不到锁会sleep。
缺点是需要额外的系统调用。
乐观锁和悲观锁
广义的定义
悲观锁
适合并发争抢中比较严重的场景
乐观锁
适合并发争抢中不严重的场景
乐观锁
进来后发现有人在做操作,不切换出去,不切换上下文,循环,对方把锁打开了,你进来,你持有这把锁,其他的人再做自旋 e
悲关锁
系统一个请求进来后,请求的线程切换出去,正在处理数据的线程处理完后,notify被等待的线程,然后让线程回到这里重新竞争这把锁
很多时候业务也会允许要悲观锁和乐观锁。
最重要的差异
乐观锁,发现对方有锁,自旋,拿到后让其他请求自旋,等一段时间问你一次。
悲观锁,请求读的时候发现有锁,请求去等待,锁处理完了,然后notify所有的等待,等待通知。
为什么悲观锁 适合并发争抢中比较严重的场景而乐观锁适合并发争抢中不严重的场景?
因为每次切换出去,需要CPU开销,要清除内存页和寄存器,然后才能在外面等。
狭义的定义
悲观锁会把整个对象加锁占为已有后才去做操作,Java中的Synchronized属于悲观锁。它认为在它修改之前,一定会有其它线程去修改它,悲观锁效率很低悲观锁有一个明显的缺点就是:它不管数据存不存在竞争都加锁,随着并发量增加,且如果锁的时间比较长,其性能开销将会变得很大。
乐观锁不获取锁直接做操作,然后通过一定检测手段决定是否更新数据,这种方式下,已经没有所谓的锁概念了,每条线程都直接先去执行操作,计算完成后检测是否与其他线程存在共享数据竞争,如果没有则让此操作成功,如果存在共享数据竞争则可能不断地重新执行操作和检测,直到成功为止。
乐观锁的核心算法是CAS(Compareand Swap,比较并交换)
读写锁
读写锁实际是一种特殊的自旋锁,允许同时有多个读者来访问共享资源,最大可能的读者数为CPU核心数。