原子操作(atomic operation)指的是由 多步操作组成的一个操作 。如果该操作不能原子地执行,则要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤的一个子集。
现代操作系统中,一般都提供了原子操作来实现一些同步操作,所谓原子操作,也就是一个独立而不可分割的操作。在单核环境中,一般的意义下原子操作中线程不会被切换,线程切换要么在原子操作之前,要么在原子操作完成之后。更广泛的意义下原子操作是指一系列必须整体完成的操作步骤,如果任何一步操作没有完成,那么所有完成的步骤都必须回滚,这样就可以保证要么所有操作步骤都未完成,要么所有操作步骤都被完成。
例如在单核系统里,单个的机器指令可以看成是原子操作(如果有编译器优化、乱序执行等情况除外);在多核系统中,单个的机器指令就不是原子操作,因为 多核系统里是多指令流并行运行 的, 一个核在执行一个指令时,其他核同时执行的指令有可能操作同一块内存区域 ,从而出现数据竞争现象。多核系统中的原子操作通常使用 内存栅障 (memory barrier)来实现,即一个CPU核在执行原子操作时,其他CPU核必须停止对内存操作或者不对指定的内存进行操作,这样才能 避免数据竞争问题 。
CAS全程为 Compare and Swap , 即比较再交换 。
jdk5增加了 并发包java.util.concurrent简称JUC ,其下面的类使用 CAS算法 实现了区别于synchronized同步锁的一种 乐观锁 。JDK 5之前Java语言是靠 synchronized 关键字保证同步的,这是一种 排斥锁也是悲观锁 。
某个资源 只给一个线程享用 称之为线程独享 反之为线程共享
在日常开发时,需要确定好哪些资源是线程共享的,共享的场景是什么.才能更好的去使用不同的 锁策略,保证对资源操作的原子性 .
class Resources {
private volatile int i = 0;
public void add() {
i++;
}
public int getI() {
return i;
}
}
复制代码
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());
}
}
复制代码
从 运行结果中得到一个结论 ,多线程对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
}
复制代码
从字节码指令看出,i++实际上是经过3个主要步骤
那么基本上可以确定 不安全的点 在于每个线程预先保留好从堆内存中获取到的i值到操作数栈( 线程临时存储区 ),然后将操作数栈的值计算后再写回到堆内存中。这样的过程就会发生数据脏读的问题了
如下图:
从图中展示如果两个线程都对 当前线程的操作数栈中的变量i进行+1 ,导致明明两个线程共加了两次结果却只加了1
这里就举例一些常用的几个类,想要了解更多的朋友们可查看 JDK中java.util.concurrent.atomic包下的类,此包下所有的类都是原子性操作的
本文将使用 AtomicInteger 这个号称保证线程安全的int类型原子包装类进行一次测试
class Resources {
private volatile AtomicInteger i = new AtomicInteger(0);
public void add() {
i.getAndAdd(1);
}
public int getI() {
return i.get();
}
}
复制代码
很显然,当我们使用AtomicInteger进行增加的时候, i在多线程的操作下准确的计算到了10000 ,这个值是正确的。但是 为什么AtomicInteger能让i线程安全呢 ?会不会是使用了 synchronized关键字 呢?让我们深入一探究竟
unsafe.getAndAddInt(this, valueOffset, delta) // this 当前对象 // valueOffset 是什么鬼? // delta 需要增加的值 // unsafe 这个又是什么鬼?这个类似乎没见过啊 复制代码
先分析这段代码
// 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有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
CAS(比较并交换)是CPU指令级的操作, 只有一步原子操作,所以非常快 。而且避免了 请求操作系统来裁定锁的问题,不用麻烦操作系统 ,直接在CPU内部就搞定了
自旋如果长时间不成功,会带来很大的性能开销。如果变更操作很耗时,同时变更很频繁,就可能导致自旋长时间不成功,带来大量的性能开销
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;
}
}
复制代码
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;
}
}
复制代码
运行结果是对的,通过 MyLock简单实现CAS的例子,应该对CAS算法的理解更加深刻