随意看看AtomicInteger类和CAS

  最近在读jdk源码,怎么说呢?感觉收获还行,比看框架源码舒服多了,一些以前就感觉很模糊的概念和一些类的用法也清楚了好多,举个很简单的例子,我在读Integer类的时候,发现了原来这个类自带缓存,看看如下代码:

package com.wyq.test;

public class TestIntegerCache {
    public static void main(String[] args) {
        String str1 = new String("127");
        String str2 = new String("127");
        Integer int1 = Integer.valueOf(str1);
        Integer int2 = Integer.valueOf(str2);
        
        System.out.println(str1 == str2);//false
        System.out.println(int1 == int2);//true
        
//-------------------------分割线---------------------------------------        
        
        String str3 = new String("128");
        String str4 = new String("128");
        Integer int3 = Integer.valueOf(str3);
        Integer int4 = Integer.valueOf(str4);
        
        System.out.println(str3 == str4);//false
        System.out.println(int3 == int4);//false
    
    }
}

   为什么127和128的结果不一样呢?因为Integer的内部维护了一个缓存,就是从jvm启动就会实例化-128~127之间的Integer对象,简单理解的话相当于这个范围内的这些对象是单实例的而且给你准备好了的,至于这个范围之外的就是多实例的了,每次获取都是新new出来的对象;(那几个包装类都类似的有缓存机制)

  咳咳,废话说的有点多了,哎慢慢啃jdk源码吧!

  这节我们来看看一个AtomicInteger类,这个类单独拿出来说一下,为什么呢?因为我感觉一般对于这个类了解不是很多,但是这个东西在多线程那一块的话经常被问到(用不用得上还要另说。。。。)

 

1.什么是原子操作

  其实我本来也不怎么清楚什么是原子类,原子类有什么用啊?我平常用的类就已经够多了,为什么还要用这个类啊?

  怎么说呢?反正很多的问题都是多线程引起来的,一个操作在多线程的情况下和单线程的情况下有的时候是由差异的(简单提一句,什么样的多线程是比较完美的多线程呢?要保证多线程环境下运行的结果和在单线程环境下运行的结果是一样的);

  一个很经典的操作:i++,这个在单线程运行下一点问题都没有,但是一到多线程环境下就很坑爹,这个i++在计算机中运行的时候会解析为i=i+1,这里其实是分为三步的:

  (1)从主存中读取i的值

  (2)将i进行加一操作

  (3)再将结果赋值到主存中i

  这里每一步都要花费一点时间,但是我们肯定是感觉不到,然而计算机却能很清楚的知道,我们简单画一个多线程的图:

    程1在执行这三步过程中,其他线程对线程1不会有任何影响!但是在这里因为i是共享的,线程1对i加一操作,此时线程1中的i等于2,线程2也执行同样的操作那么线程2中的i应该也是2,现在线程1和2都将自己的i更新到主存中,主存中的i最后就等于2。。。。。很明显线程之间相互影响,导致最后的得到的结果出问题了,这就是所谓的线程安全!原子操作的话就是将这三步给包装起来看成一个整体,能够一下子就给主存中i赋值,其他的线程影响不了(即使影响了也会有应对方法),最后就相当于类似直接i=2的操作,这就是原子操作!具体的概念网上很多。。。。

 

2.volatile关键字

  千万别说用volatile关键字保证原子性,首先这个关键字最坑,感觉这个关键字没什么人能说的清楚到底是个什么东西,为什么不能保证原子性,只能保证可见性?对于volatile关键字一般的解释如下:

  volatile赋予了变量可见——禁止编译器对成员变量进行优化,它修饰的成员变量在每次被线程访问时,都强迫从内存中重读该成员变量的值;而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存,这样在任何时刻两个不同线程总是看到某一成员变量的同一个值,这就是保证了可见性。

  这个概念每个字都认识,连在一起就不认识了。。。。

  每个线程内部还会进行这么一大串操作,可见性只能保证在前面两步Read和load,只能确保这两步从主存中获得的值一定是最新的!举个例子,线程1和线程2都从主存中读取到了数字i为0,线程1已经到了第三步use那里,而线程2执行的特别快已经将i加一等于2写入到了主存中;此时即使主存中i被volatile修饰了,但是线程1中的i是不会改变的,因为都已经在使用了还能吐出来呢?最后的结果就是线程1写入结果也是等于2。。。。。最终主存中的i等于2

  假如线程1运行的比较慢在还只是在load阶段,线程2就已经将2写入到主存中了,那么线程1就要重新到主存中去读取新的值1=2,然后再拿到自己这里进行运算,最后得出结果为3,写入主存。。。

  至于用volatile关键字测试的话,可以自己去试试,我这里就不多说了!因为重点是AtomicInteger类

 

3.AtomicInteger类

  说了这么久才开始说到正题,这个类可以解决线程原子性问题,啥也不管,先用一下看看用法再说;

  简单的创建100个线程,每个线程都对AtomicInteger中的数加1,进行1000次,最后的结果应该是100000,下面的结果就是这个;当然你可以把AtomicInteger类型的变为int类型的你就知道区别了,自己试试(运气好一两次就出结果了,运气差的试n次,或者你要适当增大线程的数量和等待时间才能看到效果,好麻烦!);

package com.wyq.test;

import java.util.concurrent.atomic.AtomicInteger;

public class TestAtomicInteger {
    //用的是一个原子类,你可以试试将这个count类型变成int count = 0,最后计算出的结果不是100000
    public static  AtomicInteger count = new AtomicInteger(0);
    
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 100; i++) {
            new Thread(new Runnable() {
                
                @Override
                public void run() {
                    for (int j = 0; j < 1000; j++) {
                        //这句话就相当于count++,自增
                        count.incrementAndGet();
                    }                    
                }
            }).start();
        }
        //主线程等20s,这种效率贼慢,但看着最简单
        //为了让创建的100个线程都执行完毕,如果没有这句话的话,那么肯定是main主线程先执行完毕,有的线程还没有创建出来,最后得到的值可能为0
        Thread.sleep(20000);
        System.out.println("最后计算的结果为:"+count);
    }

}

   那么话说回来了,这个类到底是怎么实现这种原子操作的呢?我们慢慢说,其中上面的代码中出现AtomicInteger的地方就两处,一个是传入0到构造器中实例化对象,另外一个就是incrementAndGet()方法实现自增,我把其中用到的源码拿出来看看:

public class AtomicInteger extends Number implements java.io.Serializable {
  //这个Unsafe类是一个工具类,看不到源码,有兴趣的可以通过openjdk去看看源码,大概的作用就是类似C语言中的指针,可以获取内存某处的的值
private static final Unsafe unsafe = Unsafe.getUnsafe();
  //value声明为volatile,多线程可见性
private volatile int value;
  //直接传进去一个int类型的数据
public AtomicInteger(int initialValue) { value = initialValue; }
  //自增操作,这里涉及到了一个CAS的问题,等下我们简单说说
  //这里就是一个无限循环,首先取到当前的value值,进行加一操作,这个时候会比较value的值和内存中value的值是不是一样,是一样的话说明没有其他线程对value进行修改,那就返回加一之后的值
  //否则继续这个循环
public final int incrementAndGet() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return next; } }
  //这个compareAndSet,每个单词第一个字母提取出来就是CAS,明显这个就是CAS的核心方法,通过Unsafe这个类的CAS方法(其实就是native方法,c/c++实现的)
public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }

 

 

4.CAS原理

  CAS是个什么鬼呢?说实话今天之前我也不知道居然还有这个鬼东西,不过今天一直在看很多的博客,终于大概知道了CAS是个什么东西了。。。。反正java肯定是实现不了的,必须要通过JNI调用c/c++代码才能实现这个CAS!

  我就用我的理解来说说CAS吧!举个很简单的例子,如果一个用户要对数据库中一条记录进行修改,并且要保证在这个用户还没修改成功的时候,其他用户不能对这条记录进行修改操作,一个很简单的方法:

  这应该就是CAS的基本思想,在这里我们通过version来判断数据有没有被别的用户修改过,然后根据是否满足条件来更新数据,ok,现在我们来看看CAS的概念:

  CAS(Compare-and-Swap),即比较并替换;CAS需要有3个操作数:CAS(v,e,u);v表示要更新的变量,e表示变量的预期值,u表示变量的新值。当且仅当v的实际值等于e值时,才会将v的值设为u,如果v值和e值不同,则说明已经有其他线程做了更新,则当前线程什么都不做,即更新失败。

  我稍微说一下,这三个操作数比较抽象,其实理解起来还是很容易的,对应上图中的操作:v指表中最开始查询到的version的值,e指的是表中version字段不出预料的话应该始终是0(比较乐观,认为没有人会来改变这数据),u指的是我们数据插入成功的话version的新值应该是version+1;只有我们预料version的值和原先表中version的值一样,那么才会将新的version的值更新给version字段;

  CAS应该就是这样的原理,至于对应到Atomicinteger中的unsafe.compareAndSwapInt(this, valueOffset, expect, update)这个CAS方法,是不是就比较清晰了啊,它这里的this和valueOffset这两个参数就是为了更快的找到数据value所在的位置,然后就是预料的值expect去和value比较,相等的话就将更新的值update赋值给value,如果失败的话还是会继续那个无限循环啊,去慢慢的试(也就是不断尝试获取最新值,然后进行加一,再用CAS去比较。。。),直到满足条件再更新;

  

5.CAS知识补充

  这些是我看的很多博客中觉得说的很好的东西记录一下:

  CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS操作即时没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。

  CAS其底层是通过CPU的1条指令来完成3个步骤,因此其本身是一个原子性操作,不存在其执行某一个步骤的时候而被中断的可能。

  CAS的优点:很高效的解决了原子操作问题,天生免疫死锁(根本就没有锁,当然就不会有线程一直阻塞了),更为重要的是,使用无锁的方式没有所竞争带来的开销,也没有线程间频繁调度带来的开销,他比基于锁的方式有更优越的性能

  CAS缺点:1.循环时间长开销很大;2.只能保证一个共享变量的原子操作;3.ABA问题

  顺便说说ABA问题:还是举个例子,线程一和线程二要对主存中的数据A进行操作:

    第一步:线程一读取A数据,线程二读取A数据;

    第二步:线程一从取到数据A进行自增操作然后再到将修改后的数据写入主存,这是需要花时间的,在这段时间内,线程二运行速度可能贼快,直接将主存中的数据A修改为数据B,然后又修改为数据A(或者是线程2将数据A改为B,还有个线程3将数据B改为A)

    第三步:线程一进行CAS操作,将期望的值A和内存中的值A比较,是一样的,那么线程一的CAS操作成功;线程一却不知道线程二早就对A说修改两次了,这就有点可怕了。。。

  这就是最简单版本的ABA问题(话说前几天面试,面试官问我知不知道ABA问题,怎么处理ABA问题呢?直接把我问懵了,妈耶!我还直到今天才知道CAS和ABA呢。。。。),那么怎么解决比较好呢?其实很容易,就跟我们上面那个数据库表的处理一样,弄个版本号出来,每次进行修改版本号都会进行改变,更新数据的时候会比较版本号!基于这个原理,JAVA中AtomicStampedReference这个类就实现了这种版本标记的功能,后面应该会看看这个类的源码的!



6.总结
  其实这里还有点东西没说到,AtomicInteger类的其他api就不说了,后面我会将这个类源码都看一遍然后贴出来的,有兴趣的可以看看;
  还有个要注意的地方就是AtomicInteger这个类里面实质上就是一个int类型的数据,int类型的数据就会有范围-2^31到2^31-1这么大,如果经过自增操作之后的值超过了2^31-1,那么就会出问题,那么我们就要自己实现一下避免AtomicInteger的越界问题,感兴趣的可以自己再查查这方面的资料;
  至于AtomicInteger的实际应用场景这个我还真的不知道,实在没用过!不过网上有人用的是AtomicInteger生成的数字和其他字符串拼接成一个类似uuid的东西,来表示一个唯一的资源id。。。反正我也没有使用过,有实际应用场景的请告诉我,谢谢!
  哎,读jdk源码真的会碰到很多不知道的东西,趁着还有学习的激情赶紧学,哈哈哈!对于这种原子操作以前还真的没有怎么关注过,现在看来还挺有趣的,真的很烦面试总问一些高并发之类的问题,感觉这方面自己太薄弱了,把并发包的源码看一遍应该就理解得差不多了吧!@_@
posted @ 2019-07-01 09:35  java小新人  阅读(569)  评论(0编辑  收藏  举报