CAS
简介
CAS的全称是“比较并交换”,是一种无锁的原子操作,其体现了乐观所的思想,在无锁的情况下保证线程操作共享数据的原子性。
CAS一共有3个值:
1、V:要更新的值;
2、E:预期值;
3、N:新值。
在比较和交换的过程中,需要比较V与E是否相等,若相等,则将V设置为N;若不相等,则说明有其他线程对共享变量进行了更新,当前线程应放弃。若CAS失败,则继续尝试获取新值,直至更新成功。
CAS底层实现
CAS底层是通过Unsafe类来直接调用操作系统底层的CAS指令,Unsafe类中的方法都是native方法,由系统提供接口实现,Unsafe类对CAS实现是通过C++进行的。
CAS存在的问题
ABA问题
ABA问题是指一个位置原来是A,后面被改为B,再后来又被改为A,进行CAS操作的线程无法知道在该位置的值发生过改变。
可通过加入时间戳或版本号的方式,解决ABA问题,在进行CAS操作时,需要值和版本号(或时间戳)均匹配时才能进行修改操作。在Java中,AtomicStampedReference类就实现了这种机制,它会同时检查引用值和stamp是否都相等。
循环性能开销问题
CAS在进行自旋操作时,若一直不成功,则会给CPU代理巨大的开销,可通过限制自旋次数来解决这个问题。
只能保证一个变量的原子操作
CAS只能保证对一个变量进行原子操作,当存在多个变量时,CAS无法直接保证对他们的原子操作。可通过以下两种方式解决:
1、考虑改用锁实现原子操作;
2、合并多个变量,将多个变量封装成一个对象,通过AtomicReference来保证原子性。
volatile
volatile的主要作用
保证变量在线程间的可见性
当一个线程修改某个变量的值时,volatile关键字会将修改的值刷新到主存中,该新值就对其他线程可见,对于volatile修饰的关键字,禁止使用JIT(即时编译器)进行优化。
禁止指令重排序
使用volatile修饰的变量,会在读、写共享变量时加上屏障,阻止其他读写操作越过屏障对共享变量进行读写操作。
对于volatile的写操作,会在其前后加上屏障,其中在写操作前的屏障,禁止前面的普通读操作与该写操作重排;写操作后的屏障,禁止后面的volatile读写操作与该写操作重排序,如下图所示:
对于volatile的读操作,会在其后面加上屏障,禁止后面的普通读写操作与重排序,该屏障强制让本地内存中的变量值失效,从而重新从主内存中读取最新的值,如下图所示:
对于volatile的写操作前的操作,不会被编译器重排到该写操作后;对于volatile的读操作后的操作,不会被编译器重排到该写操作前。
AQS
AQS是阻塞式锁和相关同步工具的框架,中文名为抽象队列同步器,它是构建锁或其他同步组件的基础框架。其思想是,若被请求的资源空闲,则线程申请该资源成功,反之该线程则进入一个等待队列,其他线程释放该资源时,系统随机选择一个在等待队列中的线程,赋予其资源。
AQS是悲观锁,通过Java实现,其开启和释放都需要手动进行,其工作机制是:
同步状态state由volatile修饰,保证其他线程对其可见,同时采用一个FIFO双端队列存储线程,如下图所示:
当线程要获取锁时,会尝试改变state的状态,若state状态为0,则可将其改为1,该线程抢占锁成功。若线程强制锁失败,则线程进入FIFO队列中等待。
AQS通过CAS自旋锁保证线程的原子性,保证每次只能有一个线程修改state成功。
AQS既可实现公平锁,又可实现非公平锁。当新来的线程与队列中的线程共同强资源时,该锁为非公平锁(如AQS实现类ReentrantLock),新来的线程进入等待队列中等待,只允许队列中head线程占用资源时,该锁为公平锁。
ReentrantLock
概述
ReentrantLock是可重入锁,其可中断、可设置超时时间、可设置公平锁、支持多个条件变量、支持重入。ReentrantLock主要利用CAS+AQS实现的,通过new ReentrantLock()创建的锁默认为非公平锁,要将其设置为公平锁,则应该通过有参构造函数的方式创建,并将变量设置为true(该变量设置为false时实现的非公平锁),代码:
//实现公平锁
ReentrantLock lock = new ReentrantLock(true);
构造函数如下图:
NonfairSync、FairSync的父类为Sync,Sync的父类为AQS。
加锁
调用lock()方法可实现加锁,unlock()方法实现解锁。在公平锁的条件下,锁会授予给等待时间最长的线程,在非公平锁的条件下,其加锁的方式如下:
当线程调用lock()尝试获取锁时,首先通过CAS方式修改state变量,若成功将其修改为1,则让exclusiveOwnerThread线程指向这个线程,该线程获取锁成功。若修该失败,则线程获取锁失败,则线程进入等待队列中。当exclusiveOwnerThread为null时,则持有锁的线程释放了锁,则会唤醒双向队列中在head位置的线程,公平锁和非公平锁的情况见上述AQS介绍。
为了实现锁的可重入,ReentrantLock内部有一个计数器跟踪线程持有锁的次数,当线程首次获取锁时,计数器的值变为1,如果同一线程再次获取锁,计数器增加;每释放一次锁,计数器减 1。当线程调用unlock()方法时,ReentrantLock会将持有锁的计数减1,如果计数到达0,则释放锁,并唤醒等待队列中的线程来竞争锁。
Synchronized和Lock对比
1、Synchronized是关键字,源码在JVM中,通过C++实现;而Lock是接口,源码由JDK提供,通过Java实现;
2、使用Synchronized时,退出同步代码块会自动释放锁,而使用lock时,需要通过unlock()释放;
3、两种都是悲观锁,支持互斥、同步、锁重入等功能;
4、相比于Synchronized,Lock支持获取锁状态、设置公平锁、可打断、可超时等,同时支持ReentrantLock、ReentrantReadWriteLock不同适合条件的实现;
5、在没有竞争时,Synchronized做了如偏向锁、轻量级锁等优化,但在竞争激烈时,Lock的性能更好。