资讯专栏INFORMATION COLUMN

volatile 关键字的深入分析及AtomicInteger的使用

raoyi / 1267人阅读

摘要:我们使用命令查看字节码会发现在虚拟机中这个自增运算使用了条指令。其实这么说也不是最严谨的,因为即使经过编译后的字节码只使用了一条指令进行运算也不代表这条指令就是原子操作。

volatile的语义:
1、保证被volatile修饰的变量对所有其他的线程的可见性。
2、使用volatile修饰的变量禁止指令重排优化。
看代码:

public class InheritThreadClass extends Thread{
    private static volatile int a = 0;
    @Override
    public void run() {
        for(int i = 0; i < 1000; i++){
            a++;
        }
    }
    public static void main(String[] args) {
        InheritThreadClass[] threads = new InheritThreadClass[100];
        for(int i=0; i < 100; i++){
            threads[i] = new InheritThreadClass();
            threads[i].start();
        }
        //等待所有子线程结束
        while(Thread.activeCount() > 1){
            Thread.yield();
        }
        
        //这段代码会在所有子线程运行完毕之后执行
        System.out.println(a);  //(1)
    }
}

上面的代码中创建了100个线程,然后在每个线程中对变量a进行了1000次的自增运算,那么也就意味着,如果这段代码可以正确的并发运行,最后在代码(1)处应该输出100000。但是多次运行你会发现每次输出的结果并不是我们预期的那样,而都是小于等于100000。也就是说每次运行的结果是不固定的不一样的,这是为什么呢? 因为通过上面volatile关键字的语义我们知道被该关键字修饰的变量对所有的线程是可见的啊,那怎么会出现这种情况呢?难道语义有错? 那是不可能的,语义肯定是没有错的。

我们知道每一个线程都有自己的私有内存,而线程之间的通信是通过主存来实现的,volatile在这里保证多线程的可见性的意思是说:如果一个线程修改了被volatile关键字修饰的变量,会立马刷新到主内存中,其他需要使用这个变量的线程不在从自己的私有内存中获取了,而是直接从主内存中获取。虽然volatile关键字保证了变量对所有线程的可见性,但是java代码中的运算操作并非原子操作。

我们使用javap命令查看字节码(javap -verbose InheritThreadClass.class)会发现在虚拟机中这个自增运算使用了4条指令(getstatic, iconst_1, iadd, putstatic)。 当getstatic指令把a的值压入栈顶时,volatile关键字保证了a的值此时是正确的,但是在执行iconst_1iadd这些指令时其他线程有可能已经把a的值加大了,而已经在操作栈顶的值就变成了过期的数据了,所以putstatic指令执行后可能又把较小的a值同步回主内存了。 所以它不是一个原子运算,因此在多线程的情况下它并不是一个安全的操作。其实这么说也不是最严谨的,因为即使经过编译后的字节码只使用了一条指令进行运算也不代表这条指令就是原子操作。因为一条字节码指令在解释执行时,解释器需要运行许多行代码才能实现该条指令的语义,而即使是编译执行,一条字节码指令也可能需要转化成多条本地机器码指令。

所以有关volatile的变量对其他线程的”可见性“的语义描述并不能得出这样的结论:基于volatile变量的运算在高并发下是安全的。

那这种在高并发下的自增运算如何做到线程安全呢?可以使用synchronized,但是加锁的话性能开销太大,高并发下不是一个明智之选。可以使用并发包java.util.concurrent.atomic下的AtomicInteger原子类。
看代码:

    private static volatile AtomicInteger a = new AtomicInteger(0);
    
    @Override
    public void run() {
        for(int i = 0; i < 1000; i++){
            a.getAndIncrement();
        }
    }

上面的代码就可以在高并发下正确的运行,每次输出都是100000。
看AtomicInteger源码:

**//部分关键字段**
private static final Unsafe unsafe = Unsafe.getUnsafe();
/*
  valueOffset这个是指类中相应字段在该类的偏移量, 在下面的静态块中调用objectFieldOffset()方法初始化。
*/
private static final long valueOffset;

static {
  try {
    valueOffset = unsafe.objectFieldOffset
        (AtomicInteger.class.getDeclaredField("value"));
  } catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;

//objectFieldOffset方法是一个本地方法
public native long objectFieldOffset(Field field);

// AtomicInteger的构造器之一
public AtomicInteger(int initialValue) {
    value = initialValue;
}
//getAndIncrement()这个方法的源码实现
public final int getAndIncrement() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return current;
    }
}
//get()方法的实现
public final int get() {
    return value;
}
/*compareAndSet(int expect, int update)方法内部又直接调用了
 *unsafe的compareAndSwapInt方法,这里直接上compareAndSwapInt源码的实现
 *在obj的offset位置比较内存中的值和期望的值,如果相同则更新。
 *这是一个本地方法,应该是原子的,因此提供了一种不可中断的方式更新
*/
public native boolean compareAndSwapInt(Object obj, long offset,  
                                            int expect, int update); 
    

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/66721.html

相关文章

  • 深入理解 Java 多线程系列(1)——一个简单需求并行改造 & Java多线程通信问题

    摘要:所以接下来,我们需要简单的介绍下多线程中的并发通信模型。比如中,以及各种锁机制,均为了解决线程间公共状态的串行访问问题。 并发的学习门槛较高,相较单纯的罗列并发编程 API 的枯燥被动学习方式,本系列文章试图用一个简单的栗子,一步步结合并发编程的相关知识分析旧有实现的不足,再实现逻辑进行分析改进,试图展示例子背后的并发工具与实现原理。 本文是本系列的第一篇文章,提出了一个简单的业务场景...

    ruicbAndroid 评论0 收藏0
  • int和Integer深入分析

    摘要:对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据信息并不一定要经过对象本身,这点将在节讨论。 目录介绍 1.关于int和Integer的问题区别分析 2.Integer的值缓存的原理 2.1 Java 5 中引入缓存特性 2.2 Intege...

    Half 评论0 收藏0
  • BATJ都爱问多线程面试题

    摘要:今天给大家总结一下,面试中出镜率很高的几个多线程面试题,希望对大家学习和面试都能有所帮助。指令重排在单线程环境下不会出先问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。使用可以禁止的指令重排,保证在多线程环境下也能正常运行。 下面最近发的一些并发编程的文章汇总,通过阅读这些文章大家再看大厂面试中的并发编程问题就没有那么头疼了。今天给大家总结一下,面试中出镜率很高的几个多线...

    高胜山 评论0 收藏0
  • Week 1 - Java 多线程 - Java 内存模型

    摘要:目的是解决由于多线程通过共享内存进行通信时,存在的原子性可见性缓存一致性以及有序性问题。最多只有一个线程能持有锁。线程加入规则对象的结束先行发生于方法返回。 前言 学习情况记录 时间:week 1 SMART子目标 :Java 多线程 学习Java多线程,要了解多线程可能出现的并发现象,了解Java内存模型的知识是必不可少的。 对学习到的重要知识点进行的记录。 注:这里提到的是Ja...

    zhaofeihao 评论0 收藏0
  • Java并发基础:了解无锁CAS就从源码分析

    摘要:该类将整数值与引用关联起来,可用于原子的更数据和数据的版本号。 CAS的全称为Compare And Swap,直译就是比较交换。是一条CPU的原子指令,其作用是让CPU先进行比较两个值是否相等,然后原子地更新某个位置的值,其实现方式是基于硬件平台的汇编指令,在intel的CPU中,使用的是cmpxchg指令,就是说CAS是靠硬件实现的,从而在硬件层面提升效率。 CSA 原理 利用CP...

    toddmark 评论0 收藏0

发表评论

0条评论

raoyi

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<