Java并发成神之路-精通JUC并发工具十八般武艺——CAS和不变性(五)

CAS

什么是CAS

  • 应用于并发场合,是一种思想一种算法,主要用于并发编程中,实现不可被打断的数据交换操作,从而避免多线程情况下由于线程顺序执行不能缺点而引起的问题
  • 我认为V的值应该是A,如果是的话那我就把它改成B,如果不是A(说明被别人修改过了),那我就不修改了,避免多人同时修改导致出错
  • CAS有三个操作数:内存值V,预期值A,要修改的值B,当且仅当预期值A和内存值V相同时,才将内存值修改为B,否则什么都不做,最后返回现在的V值
  • CPU的特殊指令(CAS会先进行比较,然后更新,由CPU保证原子性)

    CAS的等价代码如下所示,compareAndSwap方法发是由synchronized锁来保证原子性,而对于CAS来说,是由CPU保证的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class SimulatedCAS {

    private volatile int value;
    public synchronized int compareAndSwap(int expectedValue,int newValue){
    int oldValue = value;
    if (oldValue == expectedValue) {
    value = newValue;
    }
    return oldValue;
    }
    }

应用场景

  • 乐观锁
  • 并发容器
  • 原子类

分析在Java中是如何利用CAS实现原子操作的

  • AtomicInteger加载Unsafe工具,用来直接操作内存数据
  • Unsafe来实现底层操作
  • volatile修饰value字段,保证可见性
  • Unsafe类中的compareAndSwapInt方法
    • 方法中先想办法拿到变量value在内存中的地址
    • 通过Atomic::cmpxchg实现原子性的比较和替换,其中参数x是即将更新的值,参数e是原内存的值,至此,最终完成CAS的全过程

Unsafe类

  • Unsafe是CAS的核心类,Java无法直接访问底层操作系统,而是通过本地(native)方法来访问。不过尽管如此,JVM还是开了一个后门,JDK中有一个Unsafe,它提供了硬件级别的原子操作
  • valueOffset表示的是变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的原值的,这样我们就能通过Unsafe来实现CAS了

CAS缺点

  • ABA问题(CAS只会判断当前值是否是期待的值,并不会去判断中间是都有过更改,假设当前期待的是5,之前是5,被一个线程改成了7,然后又被一个线程改成5,此时已经被改了两次,无法判断)
  • 自旋时间过长(如果一直拿不到锁,就会一直自旋,可能会占用比较多的CPU)

以不变应万变(不可变性)

什么是不变性

  • 如果对象在被创建后,状态就不能被修改,那么它就是不可变的
  • 具有不变性的对象一定是线程安全的,我们不需要对其采取任何额外的安全措施,也能保证线程安全

final的作用

  • 类防止被继承、方法防止被重写、变量防止被修改
  • 天生是线程安全的,而不需要额外的同步开销

3中用法:修饰变量、方法、类

final修饰变量

  • 被final修饰的变量,意味着值不能被修改,如果变量是对象,那么对象的引用不能变,但是对象自身的内容依然可以变化
  • 属性被声明为final后,该变量则只能被赋值一次,且一旦被赋值,final的变量就不能再被改变,无论如何也不会变化
  • 类中的final属性
    • 第一种是在声明变量的等号右边直接赋值
    • 第二种就是构造函数中赋值
    • 第三种就是在类的初始代码块中赋值(不常用)
    • 如果不采用第一种赋值方法,那么就必须在2和3中挑一个来赋值,而不能不赋值,这是final的语法所规定的
  • 类中的static final属性
    • 第一种是在声明变量的等号右边直接赋值
    • 第二种是在static初始代码块中赋值,但是不能使用普通代码块赋值
  • 方法中final变量
    • 和前面两种不同,由于这里的变量是在方法里面的,所以没有构造函数,也不存在初始代码块
    • 不规定赋值时机,只要求在使用前必须赋值,这和方法中的非final变量的要求也是一样的

为什么要规定赋值时机

  • 如果初始化不赋值,后续赋值,就是从null变成你的赋值,这就违反了final不变原则了

final修饰方法

  • 构造方法不允许final修饰
  • 不可被重写,也就是不能被override,即便子类有同样名字的方法发,那也不是override,这个和static方法是一个道理
  • 引申:静态方法不能被重写(但是可以写重名的方法)

###final修饰类

  • 不可被继承
  • 例如典型的String类就是final的,我们从没见过那个类是继承String

注意点

  • final修饰对象的时候,只是对象的引用不变,而对象本身的属性是可以变化的
  • final使用原则:良好的编程习惯

不变性和final的关系

  • 不变性并不意味着,简单地用final修饰就是不可变
    • 对于基本数据类型,确实被final修饰后就具有不可变性
    • 但是对于对象类型,需要该对象保证自身被创建后,状态永远不会变才可以
  • 满足一下条件时,对象才是不可变的
    • 对象创建后,其状态就不能修改
    • 所有属性都是final修饰的
    • 对象创建过程中没有发发生逸出

把变量写在线程内部——栈封闭

  • 在方法里面新建的局部变量,实际上是存储在每个线程私有的栈空间,而每个栈的占用空间是不能被其他线程所访问到的,所以不会有线程安全问题,这就是著名的“栈封闭”技术,是“线程封闭”技术的一种情况
  • 方法内部的变量是不会有并发问题的(栈封闭)