并发编程的艺术 -- synchronized、volatile与CAS

版权声明:转载原创博客请附上原文链接 https://blog.csdn.net/weixin_43495590/article/details/89524467

一:初识volatile

volatile个人理解发展的目的在于减少锁的开销。锁可以保证线程的互斥性可见性,volatile仅仅完成锁的可见性,也就是线程每次读取数据都是最新数据。还需要注意一点就是volatile可以禁止指令重排序

二:可见性问题

数据存储划分为线程内存即副内存和主内存,线程修改后的数据存在于副内存中未来得及刷新到主内存,其它内存读取主内存数据造成线程之间数据可见性问题。如下图所示:
内存示意图

三:可见性使用场景

正如第一点所说,volatile不能用于线程互斥性场景,也就是不能保证原子性操作,只能保证数据修改后的及时通知

class ThreadA extends Thread{
    @Override
    public void run() {
        ++TestClass.i;
    }
}

public class TestClass {

    public static Integer i = 1;

    @SneakyThrows
    public static void main(String[] args) {
        for (int i = 0;i < 100;i++){
            new  ThreadA().start();
        }
        Thread.sleep(5000);
        System.out.println(i);
    }

}
  1. 上述代码启动100根线程对"i"进行加一操作,最后正常结果应为101,但是最后有98、99、100
  2. 分析原因可能会说是因为++操作非原子操作,将"i"修改为AtomicInteger类型后经过N次试验还是会出现错误情况
  3. 再分析就只能是因为内存原因,给i加上volatile关键字后跑脚本执行100万次也没有出现异常。当然将run()加上synchronized也可以解决问题

四:指令重排序

各种博客上都在使用单例模式举例,分配内存为初始化返回错误对象的例子确实经典。指令重排序是因为虚拟机优化产生的问题,volatile可以在编译后的字节码开头加入Lock汇编指令解决这个问题

五:初识Synchronized

对象头中加锁,每个对象的对象头都会记录锁的状态。实例方法默认锁this、静态方法默认锁class对象、代码块自定义锁对象。Synchronized这把重锁也有被优化,锁的状态被划分为偏向锁、轻锁、重锁

六:Synchronized优化的偏向锁

概念提出的基础是锁不存在多线程竞争,并且总是由同一线程获得锁。设置偏向锁启动后开始时间-XX:BiasedLockingStartupDelay=0,当确定大多数线程处于竞争不适合偏向锁使用参数关闭-XX:-UseBiasedLocking=false

操作 描述
加锁 检测对象头运行时数据区域是否有指向当前线程偏向锁,成功就获得了锁。失败检查偏向锁标志位是否为1,否就使用CAS竞争,是就尝试使用CAS将对象头锁标志指向当前线程
撤销 其它线程竞争才释放,全局安全点暂停线程。检测线程存活,否将对象头设置为无锁,是遍历偏向对象锁记录,要么偏向其它线程、设置为无锁、标记对象不适合做偏向锁,环形暂停线程

偏向锁加锁流程

七:乐观锁CAS实现

采用非阻塞算法即一个线程的失败挂起不应该影响其它线程失败挂起的算法。内存值、预测值、最新值,自旋原子操作比较判断内存值与预测值相等再进行替换。以下使用AtomicInteger为例查看实现:

	/// 使用volatile关键字修饰内存值value,保证线程可见性
    private volatile int value;
    
    public final int getAndUpdate(IntUnaryOperator updateFunction) {
        int prev, next;
        do {
        	// 内存值获取
            prev = get();
            // 计算新值
            next = updateFunction.applyAsInt(prev);
            // 内存值与预测值比较并更新新值
        } while (!compareAndSet(prev, next));
        return prev;
    }

八:CAS问题

8.1 ABA问题

首先是ABA问题,内存值为A,线程修改为B后又修改为A,这时候其它线程进行也会被认定为未修改。解决方案是给内存值添加版本号,Java中提供AtomicStampedReference解决版本问题

class ThreadA extends Thread{
    @Override
    public void run() {
        for(;;){
            Integer reference = TestClass.asr.getReference();
            int stamp = TestClass.asr.getStamp();
            Integer newReference = reference + 1;
            Integer newStamp = stamp + 1;
            if(TestClass.asr.compareAndSet(reference, newReference, stamp, newStamp)){
                break;
            }

        }
    }
}

public class TestClass {

    public static volatile AtomicStampedReference<Integer> asr = new AtomicStampedReference<>(1,1);

    @SneakyThrows
    public static void main(String[] args) {
        for (int i = 0;i < 9999;i++){
            new ThreadA().start();
        }
        Thread.sleep(5000);
        System.out.println(asr.getReference());
    }
}

8.2 空旋浪费资源问题

乐观锁都会存在自旋操作,如果自旋操作不成功那就意味着不停的循环,所以务必记住乐观锁的使用场景是假设锁竞争不激烈的情况下

猜你喜欢

转载自blog.csdn.net/weixin_43495590/article/details/89524467