CAS
什么是原子(Atom)操作:
多线程中的原子操作类似于数据库中的同时执行AB两个语句,要么同时执行成功,要么同时执行失败。
synchronize的不足:
- syn是基于阻塞的锁机制,颗粒度还是比较大 的。
- 如果被阻塞的线程优先级很高怎么办。
- 拿到锁的线程一直不释放锁怎么办。
- 如果出现大量竞争会消耗CPU,同时带来死锁或其他安全隐患。
用syn也可以实现原子操作不过不太合适,目前CPU指令级别实现了将比较和交换(Conmpare And Swap)进行原则性的操作(CAS不是锁只是CPU提供的一个原子性操作指令哦切记),它的实现步骤如下
- 获得L(内存地址)上的数据初始值D1
- 对D1的数据进行增减后最终等到D2
- 尝试将D2 放到原来L的位置上。
- 放之前先比较目前L里的数据是否跟我之前取出的D1值跟版本号都对应。
- 对应了 我就将数据放到L中,单但有一个不对应则写入失败。重新执行步骤1.
- 上面的步骤如果失败了就会重复进入一个1~5的死循环,俗称自旋。
CAS
在语言层面不进行任何处理,直接将原则操作实现在硬件级别实现,只所以可以实现硬件级别的操作核心是因为CAS操作类中有个核心类UnSafe
类,
Java
和C++
语言的一个重要区别就是Java中我们无法直接操作一块内存区域,不能像C++中那样可以自己申请内存和释放内存。Java
中的Unsafe
类为我们提供了类似C++手动管理内存的能力。Unsafe
类,全限定名是sun.misc.Unsafe
,UnSafe
类中所有的方法都是native
修饰的,也就是说UnSafe
类中的方法都是直接调用操作底层资源执行响应的任务。主要功能如下:
用CAS的弊端:
- ABA 问题
现象:在内存中数据变化为A==>B==>A,这样如何判别,因为这样其实数据已经修改过了。
加粗样式解决方法:引入版本号
- 开销问题
如果长期不成功那就会进入自旋。
JVM支持处理器提供的pause指令,使得效率会有一定的提升,pause指令有两个作用:
- 它可以延迟流水线执行指令,使CPU不会消耗过多的执行资源,
- 它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
- 只能保证一个共享变量之间的原则性操作
问题描述:当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
解决办法:从JDK5开始提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
JDK中相关原子操作类的使用
- 更新基本类型类:AtomicBoolean,AtomicInteger,AtomicLong
- 更新数组类:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
- 更新引用类型:AtomicStampedReference,AtomicMarkableReference,AtomicReference
- 原子更新字段类: AtomicReferenceFieldUpdater,AtomicIntegerFieldUpdater,AtomicLongFieldUpdater
相互之间差别不太,我们以AtomicInteger为例,常用方法:- get()
- set(int)
- getAndIncrement()
- incrementAndGet()
…
AtomicInteger 例子:
static AtomicInteger ai = new AtomicInteger(10);
public static void main(String[] args) {
System.out.println(ai.getAndIncrement());//10--->11
System.out.println(ai.incrementAndGet());//11--->12--->out
System.out.println(ai.get());
}
用引用类型AtomicReference
包装user
对象,然后修改包装后的对象,user
本身参数是不变的这点要切记。
public class UseAtomicReference {
static AtomicReference<UserInfo> userRef = new AtomicReference<UserInfo>();
public static void main(String[] args) {
UserInfo user = new UserInfo("sowhat", 14);//要修改的实体的实例
userRef.set(user); // 引用包装后,包装里面的类跟包装前是两个不同的对象。
UserInfo updateUser = new UserInfo("liu", 12);//要变化的新实例
userRef.compareAndSet(user, updateUser);
System.out.println(userRef.get().getName());
System.out.println(userRef.get().getAge());
System.out.println(user.getName()); // 注意此时的user 属性
System.out.println(user.getAge());
}
//定义一个实体类
static class UserInfo {
private String name;
private int age;
public UserInfo(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
}
ABA问题:JDK提供了两个类
- AtomicStampedReference: 返回Boolean值,关心的是动没动过。
- AtomicMarkableReference:关心的是动过几次。
我们以AtomicStampedReference
为例分析:
重点函数如下:
AtomicStampedReference(V initialRef, int initialStamp)
,V表示要CAS的数据,int表示初始化版本。public V getReference()
表示获得CAS里面的数据public int getStamp()
表示获得当前CAS版本号- 第一个参数是原来的CAS中原来参数,第二个参数是要替换后的新参数,第三个参数是原来CAS数据对于版本号,第四个参数表示替换后的新参数版本号。
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp)
具体demo加深理解如下:
public class UseAtomicStampedReference {
static AtomicStampedReference<String> asr = new AtomicStampedReference<>("sowhat", 0);
public static void main(String[] args) throws InterruptedException {
final int oldStamp = asr.getStamp(); // 那初始的版本号
final String oldReferenc = asr.getReference(); // 初始数据
System.out.println(oldReferenc + "----------" + oldStamp);
Thread rightStampThread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()
+ "当前变量值:" + oldReferenc + "当前版本戳:" + oldStamp + "-"
+ asr.compareAndSet(oldReferenc, oldReferenc + "Java",
oldStamp, oldStamp + 1));
}
});
Thread errorStampThread = new Thread(new Runnable() {
@Override
public void run() {
String reference = asr.getReference();
System.out.println(Thread.currentThread().getName()
+ "当前变量值:" + reference + "当前版本戳:" + asr.getStamp() + "-"
+ asr.compareAndSet(reference, reference + "C",
oldStamp, oldStamp + 1)); //此处版本号用错了
}
});
rightStampThread.setName("对的线程");
rightStampThread.start();
rightStampThread.join();
errorStampThread.setName("错的线程");
errorStampThread.start();
errorStampThread.join();
System.out.println(asr.getReference() + "------------" + asr.getStamp());
}
}
显示锁Lock
synchronized特性:
- java中的一个关键字,也就是说说Java语言里的内置锁。
- 是独占锁,加锁和解锁的过程自动进行,易于操作,但不够灵活。
- 不可响应中断,一个线程获取不到锁就一直等着。
- 可重入,因为加锁和解锁自动进行,不必担心最后是否释放锁。
因为syn有诸多不变尤其是不可timeout
,因此在JDK5以后引入了Lock
这个interface
,相比于syn它具有如下特性:
- Lock是Java代码级别的锁。
- 独占锁,加锁和解锁的过程需要手动进行,不易操作,但非常灵活
- 可重入,但加锁和解锁需要手动进行,且次数需一样,否则其他线程无法获得锁。
- 关键是可中断==,可以实现timeout跟interrupted
日常经常用Lock的实现类ReentrantLock
:
public class LockDemo {
private Lock lock = new ReentrantLock();
private int count;
public void increament() {
lock.lock();
try {
count++;
}finally {
lock.unlock();
}
}
public synchronized void incr2() {
count++;
incr2();
}
// 可重入锁 底层类似 累加器 锁的调用
public synchronized void test3() {
incr2();
}
}
Lock还有一个重点是可以实现公平锁跟非公平锁:
如果在时间上,先对锁进行获取对请求,一定被先满足则锁公平的。如果不满足就是非公平的。
- 公平锁
比如ABC三个任务去抢同一个锁,A先获得 BC就要被依此挂起
挂起的含义是指:本来线程是占用cpu资源的,但是如果挂起的话,操作系统就不给这个现成分配cpu资源,除非以后再恢复,所以线程挂起的作用就是节省cpu资源,跟wait()还有sleep()是不一样的。
BC被挂起就相当于AE86被喊停了,等A再用完锁,BC才获得锁再起步这个过程对于CPU来说是很耗时的。
- 非公平锁
比如ABC三个任务抢同一个锁,A获得锁在运行但时间长,而B提交后由于非公平机制会直接进行抢锁再执行。所以非公平锁相对来说性能会更好些。
ReentrantLock 底层默认实现为非公平锁:
public ReentrantLock() {
sync = new NonfairSync();// 默认非公平锁
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
syn跟Lock 使用建议:
- 能用syn就用syn,代码更简洁。
- 需要锁可中断,超时获取锁,尝试获取锁时候 用Lock。
Condition
synchronized
可用wait()
和notify()/notifyAll()
方法相结合可以实现等待/通知模式。ReentrantLock
也提供了Condition
来提供类似的功能。
public Condition newCondition() {
return sync.newCondition();
}
其中 Condition
主要函数如下
基本跟syn
的操作差别不大,唯一区别可能就是多来个a
在方法前面。
读写锁
前面说到的syn
跟Lock
都是独占锁,
ReadWriteLock
接口实现,其实就是有两个锁,一个管读操作,一个管写操作,对于多度少写的场景一般比syn
性能可提速10倍。
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
具体实现类是ReentrantReadWriteLock
。
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {
private static final long serialVersionUID = -6992448646407690164L;
/** Inner class providing readlock */
private final ReentrantReadWriteLock.ReadLock readerLock;// 单独的 读锁
/** Inner class providing writelock */
private final ReentrantReadWriteLock.WriteLock writerLock;
//单独的写锁
测试读写锁跟syn性能,比如我们是买娃娃的,有总销售额跟库存数,没减少库存则销售额会增加,我们用多线程来执行。
GoodsInfo 商品信息类
public class GoodsInfo {
private final String name;
private double totalMoney;//总销售额
private int storeNumber;//库存数
public GoodsInfo(String name, int totalMoney, int storeNumber) {
this.name = name;
this.totalMoney = totalMoney;
this.storeNumber = storeNumber;
}
public void changeNumber(int sellNumber){
this.totalMoney += sellNumber*25;
this.storeNumber -= sellNumber;
}
}
操作类接口
public interface GoodsService {
GoodsInfo getNum() throws Exception;//获得商品的信息
void setNum(int number) throws Exception;//设置商品的数量
}
操作类实现Syn
public class UseSyn implements GoodsService {
private GoodsInfo goodsInfo;
public UseSyn(GoodsInfo goodsInfo) {
this.goodsInfo = goodsInfo;
}
@Override
public synchronized GoodsInfo getNum() throws Exception {
TimeUnit.MILLISECONDS.sleep(5);
return this.goodsInfo;
}
@Override
public synchronized void setNum(int number) throws Exception {
TimeUnit.MILLISECONDS.sleep(5);
goodsInfo.changeNumber(number);
}
}
操作类实现读写锁
public class UseRwLock implements GoodsService {
private GoodsInfo goodsInfo;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock getLock = lock.readLock(); //读锁
private final Lock setLock = lock.writeLock(); //写锁
public UseRwLock(GoodsInfo goodsInfo) {
this.goodsInfo = goodsInfo;
}
@Override
public GoodsInfo getNum() {
getLock.lock();// 加读锁
try {
SleepTools.ms(5);
return this.goodsInfo;
} finally {
getLock.unlock();
}
}
@Override
public void setNum(int number) {
setLock.lock(); //加写锁
try {
SleepTools.ms(5);
goodsInfo.changeNumber(number);
} finally {
setLock.unlock();
}
}
}
真正的测试性能,多度少些并发。
public class BusiApp {
static final int readWriteRatio = 10;//读写线程的比例
static final int minthreadCount = 3;//最少线程数
//读操作
private static class GetThread implements Runnable {
private GoodsService goodsService;
public GetThread(GoodsService goodsService) {
this.goodsService = goodsService;
}
@Override
public void run() {
long start = System.currentTimeMillis();
for (int i = 0; i < 100; i++) {//操作100次
try {
goodsService.getNum();
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "读取商品数据耗时:"
+ (System.currentTimeMillis() - start) + "ms");
}
}
//写操做
private static class SetThread implements Runnable {
private GoodsService goodsService;
public SetThread(GoodsService goodsService) {
this.goodsService = goodsService;
}
@Override
public void run() {
long start = System.currentTimeMillis();
Random r = new Random();
for (int i = 0; i < 10; i++) {//操作10次
SleepTools.ms(50);
try {
goodsService.setNum(r.nextInt(10));
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "写商品数据耗时:" + (System.currentTimeMillis() - start) + "ms---------");
}
}
public static void main(String[] args) throws InterruptedException {
GoodsInfo goodsInfo = new GoodsInfo("Cup", 100000, 10000);
//GoodsService goodsService = new UseRwLock(goodsInfo); //单次耗时770ms 用读写锁实现
GoodsService goodsService =new UseSyn(goodsInfo); //单次耗时 17000ms 用syn实现
for (int i = 0; i < minthreadCount; i++) {
Thread setT = new Thread(new SetThread(goodsService));
for (int j = 0; j < readWriteRatio; j++) {
Thread getT = new Thread(new GetThread(goodsService));
getT.start();
}
SleepTools.ms(100);
setT.start();
}
}
}
Syn性能
读写锁性能