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
11public 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修饰的
- 对象创建过程中没有发发生逸出
把变量写在线程内部——栈封闭
- 在方法里面新建的局部变量,实际上是存储在每个线程私有的栈空间,而每个栈的占用空间是不能被其他线程所访问到的,所以不会有线程安全问题,这就是著名的“栈封闭”技术,是“线程封闭”技术的一种情况
- 方法内部的变量是不会有并发问题的(栈封闭)