原子操作是什么?
原子操作(atomic operation)指的是由多步操作组成的一个操作。如果该操作不能原子地执行,则要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤的一个子集。
现代操作系统中,一般都提供了原子操作来实现一些同步操作,所谓原子操作,也就是一个独立而不可分割的操作。在单核环境中,一般的意义下原子操作中线程不会被切换,线程切换要么在原子操作之前,要么在原子操作完成之后。更广泛的意义下原子操作是指一系列必须整体完成的操作步骤,如果任何一步操作没有完成,那么所有完成的步骤都必须回滚,这样就可以保证要么所有操作步骤都未完成,要么所有操作步骤都被完成。
例如在单核系统里,单个的机器指令可以看成是原子操作(如果有编译器优化、乱序执行等情况除外);在多核系统中,单个的机器指令就不是原子操作,因为多核系统里是多指令流并行运行的,一个核在执行一个指令时,其他核同时执行的指令有可能操作同一块内存区域,从而出现数据竞争现象。多核系统中的原子操作通常使用内存栅障(memory barrier)来实现,即一个CPU核在执行原子操作时,其他CPU核必须停止对内存操作或者不对指定的内存进行操作,这样才能避免数据竞争问题。
CAS是什么?
CAS全程为Compare and Swap,即比较再交换。
jdk5增加了并发包java.util.concurrent简称JUC,其下面的类使用CAS算法实现了区别于synchronized同步锁的一种乐观锁。JDK 5之前Java语言是靠synchronized关键字保证同步的,这是一种排斥锁也是悲观锁。
线程独享和线程共享及java的锁种类
某个资源只给一个线程享用称之为线程独享反之为线程共享
在日常开发时,需要确定好哪些资源是线程共享的,共享的场景是什么.才能更好的去使用不同的锁策略,保证对资源操作的原子性.
java的锁种类分为8种
- 公平锁/非公平锁
- 可重入锁
- 独享锁/共享锁
- 互斥锁/读写锁
- 乐观锁/悲观锁
- 分段锁
- 偏向锁/轻量级锁/重量级锁
- 自旋锁
不具备原子性操作的例子
资源类Resources
class Resources {
private volatile int i = 0;
public void add() {
i++;
}
public int getI() {
return i;
}
}
复制代码
测试类Demo
public class Demo {
public static void main(String[] args) {
// 资源类实例
final Resources resources = new Resources();
// 定长线程池-线程数10个
ExecutorService executorService = Executors.newFixedThreadPool(10);
// 循环打印-启动10个线程
for (int i = 0; i < 10; i++) {
executorService.execute(new Runnable() {
public void run() {
// 每个线程对资源类的i进行+1
for (int i1 = 0; i1 < 1000; i1++) {
resources.add();
}
}
});
}
// 阻塞主线程 - 等待线程池执行完毕
executorService.shutdown();
while (!executorService.isTerminated()) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 打印i的值,预期值应该是 10*1000*1 = 10000 才算合理
System.out.println("I的值打印为:"+resources.getI());
}
}
复制代码
运行结果为:I的值打印为:8939
从运行结果中得到一个结论,多线程对Resources资源类中的add()方法进行i++,是线程不安全的。
有的小伙伴就很奇怪了,只是一行i++代码为什么会是不安全的呢?不是说多步操作由于CPU多核同时运行才会不安全吗?可i++明明就一行代码啊
其实我一开始也是这么认为的,为什么一行代码会出现线程不安全的问题,于是带着疑问反编译了java代码后发现i++是一行代码但是却不是一步操作.反编译命令:javap -c Resources.class
反编译后的Resources代码如下:
class com.cas.Resources {
com.cas.Resources();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_0
6: putfield #2 // Field i:I
9: return
public void add();
Code:
0: aload_0
1: dup
2: getfield #2 // Field i:I
5: iconst_1
6: iadd
7: putfield #2 // Field i:I
10: return
public int getI();
Code:
0: aload_0
1: getfield #2 // Field i:I
4: ireturn
}
复制代码
仔细观察add方法翻译JVM指令如下
- aload_0 : 从局部变量表的相应位置装载一个对象引用到操作数栈的栈顶
- dup : 表示数据重复定义,也就是复制操作数
- getfield : 获取I存放在堆的值
- iconst_1 : 当int取值1~5时,JVM采用iconst指令将常量压入栈中
- iadd : 计算i的值,说白了就是i+1
- putfield : 将计算I的结果写到堆内存中
- return : 结束方法
从字节码指令看出,i++实际上是经过3个主要步骤
- 从堆内存中获取到i的值并且复制一份到当前线程的操作数栈中
- 从当前线程的操作数栈中去计算i的值
- 将计算后结果写回到堆内存中的i去
那么基本上可以确定不安全的点在于每个线程预先保留好从堆内存中获取到的i值到操作数栈(线程临时存储区),然后将操作数栈的值计算后再写回到堆内存中。这样的过程就会发生数据脏读的问题了
如下图:
从图中展示如果两个线程都对当前线程的操作数栈中的变量i进行+1,导致明明两个线程共加了两次结果却只加了1
JUC中的原子性操作类
- AtomicInteger
- AtomicLong
- AtomicBoolean
- AtomicReference
这里就举例一些常用的几个类,想要了解更多的朋友们可查看JDK中java.util.concurrent.atomic包下的类,此包下所有的类都是原子性操作的
本文将使用AtomicInteger这个号称保证线程安全的int类型原子包装类进行一次测试
资源类稍微做下修改,将int i 声明为AtomicInteger i
资源类
class Resources {
private volatile AtomicInteger i = new AtomicInteger(0);
public void add() {
i.getAndAdd(1);
}
public int getI() {
return i.get();
}
}
复制代码
运行结果为:I的值打印为:10000
很显然,当我们使用AtomicInteger进行增加的时候,i在多线程的操作下准确的计算到了10000,这个值是正确的。但是为什么AtomicInteger能让i线程安全呢?会不会是使用了synchronized关键字呢?让我们深入一探究竟
点进去发现只用了一行代码搞定,而且还没有锁相关代码,但是调用了一个方法,不会是那个方法加了锁吧,来猜测观察一下调用getAndAddInt方法的三个传参是什么意思
unsafe.getAndAddInt(this, valueOffset, delta)
// this 当前对象
// valueOffset 是什么鬼?
// delta 需要增加的值
// unsafe 这个又是什么鬼?这个类似乎没见过啊
复制代码
那我们再点进去看一下unsafe.getAndAddInt(this, valueOffset, delta)究竟做了什么,即使是直接操作内存那也不能保证线程安全啊
看完吓一跳,写个where循环在里面调用compareAndSwapInt(var1, var2, var5, var5 + var4)方法,那能猜想到的应该是这个线程去尝试着抢锁,有可能抢不到,然后挂起当前线程不停去自旋抢锁直到抢到锁并且增加成功为止
先分析这段代码
// var1 当前对象
// var2 当前对象值的位移量
// var4 需要增加的值
// var5 声明他作甚?
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
// 再看看这行代码
// var5 = this.getIntVolatile(var1, var2); 根据当前对象和当前对象值的位移量 获取内存中最新的值
// 再往下看看这行代码
// compareAndSwapInt(var1, var2, var5, var5 + var4) 再想想cas的全称即比较再交换
// var1 当前对象
// var2 当前对象值的位移量
// var5 当前对象在内存中最新的值
// (var5 + var4) 换成计算后的值
复制代码
原来如此,这像不像mysql中innodb的行锁特性
update stu set status=2 where id = 1 and status=1
// 通过ID确定好索引
// 通过索引锁定数据再判断status=1
// 才将id为1的行修改成status=2
复制代码
得出结论,这就是cas实现乐观锁的方式,先比较再进行赋值
CAS算法的好处
CAS是一种无锁算法,通过硬件层面上对先后操作内存的线程进行排队处理,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
CAS(比较并交换)是CPU指令级的操作,只有一步原子操作,所以非常快。而且避免了请求操作系统来裁定锁的问题,不用麻烦操作系统,直接在CPU内部就搞定了
CAS算法的弊端
刚刚看到一个问题,如果增加不成功,那while会一直尝试着去增加,会不会产生死锁或导致线程耗时太久的一系列问题发生啊?
-
高并发且自旋不成时
自旋如果长时间不成功,会带来很大的性能开销。如果变更操作很耗时,同时变更很频繁,就可能导致自旋长时间不成功,带来大量的性能开销
手写一个简单的锁
public class MyLock implements Lock {
/**
* 锁的拥有者
*/
private AtomicReference<Thread> atomicReference = new AtomicReference();
/**
* 线程等待队列
*/
private LinkedBlockingQueue<Thread> linkedBlockingQueue = new LinkedBlockingQueue<Thread>();
/**
* 加锁
*/
public void lock() {
if (!tryLock()) {
// 如果抢锁失败,将线程进入等待队列,并挂起当前线程
linkedBlockingQueue.offer(Thread.currentThread());
// 挂起当前线程方式有 suspend park wait
// suspend已被弃用了,wait必须配合synchronized才能使用,所以合适我们的也只有park了
// 线程之间的唤醒有可能是伪唤醒,所以需要写死循环
while (true) {
// 只有队列头部的线程等于当前线程才进行抢锁,否则挂起
if (linkedBlockingQueue.peek() == Thread.currentThread()) {
// 抢不到锁就挂起,抢到锁出队列并且退出lock方法
if (!tryLock()) {
LockSupport.park();
} else {
linkedBlockingQueue.poll();
return;
}
} else {
LockSupport.park();
}
}
}
}
/**
* 尝试抢锁
*/
public boolean tryLock() {
// 如果锁的拥有者为null,那就将他设为当前线程
return atomicReference.compareAndSet(null, Thread.currentThread());
}
/**
* 释放锁
*/
public void unlock() {
// 尝试释放锁成功后-唤醒队列头部线程
if (atomicReference.compareAndSet(Thread.currentThread(), null)) {
Thread peek = linkedBlockingQueue.peek();
if (peek != null) {
LockSupport.unpark(peek);
}
}
}
public void lockInterruptibly() throws InterruptedException {
}
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
public Condition newCondition() {
return null;
}
}
复制代码
测试:资源类稍微做下修改,将AtomicInteger i 声明为int i
class Resources {
/**
* 自定义锁
*/
private MyLock myLock = new MyLock();
/**
* 非原子操作的int类型
*/
private volatile int i = 0;
public void add() {
myLock.lock();
try {
i++;
} finally {
myLock.unlock();
}
}
public int getI() {
return i;
}
}
复制代码
运行结果为:I的值打印为:10000
运行结果是对的,通过MyLock简单实现CAS的例子,应该对CAS算法的理解更加深刻