原子操作(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算法的理解更加深刻