JUC-CAS详解

Posted 玩葫芦的卷心菜

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JUC-CAS详解相关的知识,希望对你有一定的参考价值。

CAS解析

CAS(CompareAndSwap):比较并替换

讲到CAS肯定要说到AtomicInteger等一系列原子类
比如AtomicInteger自增操作时如何保证原子操作的
其中就是利用了CAS自旋锁,CAS通过Unsafe来通过JVM本地方法操作内存指针完成

其中一个重要方法

	public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

通过this当前对象和valueOffset内存偏移获取对象内存所在值,用expect获取的主内存拷贝作比较,如果相同就替换为update,不同就失败,一直自旋直到成功

volatile int value=xxx;

public V fun(int update)
	while(true){
		int old=value;//读取值
		//毕竟内存值和旧值,如果相同表示没有被别的线程修改过,内存值就替换为update
		//如果失败就循环
		if(compareAndSet(old,update)){
			break;
		}
	}
}

通过这种特点,没有加锁,提高了并发度

CAS缺点:
循环时间过长
可以维护的变量只有一个,多个就需要加锁

ABA问题

ABA问题:
有两个线程需要对同一个值作更新,值为A
线程一和二都已经从主内存拷贝到A
线程一准备将A拷贝为X
但是线程二比线程一快的多,先一步将A更新为B,然后将B又更新为A
线程一做CAS的时候进行比较一致,更新成功

这种情况对于不在意过程只在意结果的情况没有问题
但是在意过程的情况肯定是不行的

在解决ABA问题之前,先了解 AtomicReference,可以通过泛型对对象进行原值操作

User z3=new User(1,"张三");
User l4=new User(2,"李四");
AtomicReference<User> atomicReference=new AtomicReference<>();
//设置值为z3
atomicReference.set(z3);

//通过比较z3和内存值来判断是否替换为l4
System.out.println(atomicReference.compareAndSet(z3, l4)+"\\t data:"+atomicReference.get().toString());

ABA情况演示:

//        初始化为100
        AtomicReference<Integer> atomicReference=new AtomicReference<>(100);

        //线程计数
        CountDownLatch countDownLatch=new CountDownLatch(2);
        new Thread(()->{
            try {
//                等待t2获取值
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            atomicReference.compareAndSet(100,101);
            atomicReference.compareAndSet(101,100);

            countDownLatch.countDown();
        },"t1").start();

        new Thread(()->{
            int oldVal=atomicReference.get();
            try {
//                等待t1修改ABA
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            atomicReference.compareAndSet(oldVal,102);

            countDownLatch.countDown();
        },"t2").start();


        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("最终结果:"+atomicReference.get());

最后还是更新为102了

为了解决ABA,可以为其加上版本号,每次线程对原子对象做更新时,版本号也随之更新
通过匹配内存值与旧值以及匹配版本号是否相等两个条件来判断是否替换,当然这个版本号肯定大部分情况是需要唯一不相同的
时间戳正好满足这个特点
所以提供了AtomicStampedReference类,能够初始化对象的情况也携带版本号

通过AtomicStampedReference解决ABA:

AtomicStampedReference<String> atomicStampedReference=new AtomicStampedReference<>("A",1);

        CountDownLatch countDownLatch=new CountDownLatch(2);
        new Thread(()->{
            int stamp=atomicStampedReference.getStamp();
            System.out.println("第一次获取版本号:"+stamp);

            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

//            内存值对比A,版本号对比 如果一致就更新为B,且更新版本号
            atomicStampedReference.compareAndSet("A","B",atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
            System.out.println("更新后版本号:"+atomicStampedReference.getStamp());

//            B更新为A
            atomicStampedReference.compareAndSet("B","A",atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
            System.out.println("更新后版本号:"+atomicStampedReference.getStamp());

            countDownLatch.countDown();
        },"t1").start();

        new Thread(()->{
            int stamp=atomicStampedReference.getStamp();
            String oldVal=atomicStampedReference.getReference();
            System.out.println("第一次获取版本号:"+stamp);

            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

//            通过已经获取到的旧值和版本号做更新
            atomicStampedReference.compareAndSet(oldVal,"X",stamp,stamp+1);


            countDownLatch.countDown();
        },"t2").start();


        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("最终结果:"+atomicStampedReference.getReference());

自旋锁

没有使用阻塞,cpu就不用切换(阻塞和唤醒需要再内核态和用户态来回切换),减少了开销
采用循环判读的方式获取锁,增强了并发性,但是循环时间过长会影响效率

class SpinLock{
    AtomicReference<Thread> atomicReference=new AtomicReference<>();

    public void lock(){
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName()+"准备获取锁");

        //如果当前值为空,表示没有线程获取锁,就将当前线程替换,发挥true !true=false退出循环获取锁
        //那么其他线程来比较的时候,发现不为空,表示锁已经被其他线程获得,就会一直循环等待
        while(!atomicReference.compareAndSet(null,thread)){

        }
        System.out.println(thread.getName()+"获得锁");
    }

    public void unlock(){
        Thread thread = Thread.currentThread();
        //如果当前线程占用锁,就释放掉,替换为空,加锁循环就可以获取锁了
        atomicReference.compareAndSet(thread,null);
        System.out.println(thread.getName()+"释放锁");
    }
}

//多个线程抢占自旋锁
public class DemoSpinLock {
    public static void main(String[] args) {
        SpinLock spinLock=new SpinLock();

        new Thread(()->{
            spinLock.lock();
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLock.unlock();
        },"threadA").start();

        new Thread(()->{
            spinLock.lock();
            spinLock.unlock();
        },"threadB").start();

        new Thread(()->{
            spinLock.lock();
            spinLock.unlock();
        },"threadC").start();
    }
}

以上是关于JUC-CAS详解的主要内容,如果未能解决你的问题,请参考以下文章

(转) Java中的负数及基本类型的转型详解

详解Android WebView加载html片段

14.VisualVM使用详解15.VisualVM堆查看器使用的内存不足19.class文件--文件结构--魔数20.文件结构--常量池21.文件结构访问标志(2个字节)22.类加载机制概(代码片段

Python中verbaim标签使用详解

Yii2片段缓存详解

DOM探索之基础详解——学习笔记