资讯专栏INFORMATION COLUMN

AtomicInteger原理

tuantuan / 3045人阅读

摘要:提供这些原子类的目的就是为了解决基本类型操作的非原子性导致在多线程并发情况下引发的问题。测试代码引发的线程问题最终的值为如果是原子操作,那么结果应该就是,反复运行几次发现结果大部分情况下都不是,这也证明了的非原子性在多线程下产生的问题。

AtomicInteger的原理

java的并发原子包里面提供了很多可以进行原子操作的类,比如:

AtomicInteger

AtomicBoolean

AtomicLong

AtomicReference

等等,一共分为四类:原子更新基本类型(3个)、原子更新数组、原子更新引用和原子更新属性(字段)。、提供这些原子类的目的就是为了解决基本类型操作的非原子性导致在多线程并发情况下引发的问题。那么非原子性的操作会引发什么问题呢?下面我们通过一个示例来看一下。

1. i++引发的问题

我们知道基本类型的赋值操作是原子操作,但是类似这种i++的操作并不是原子操作,通过反编译代码我们可以大致了解此操作分为三个阶段:

tp1 = i;  //1
tp2 = tp1 + 1;  //2
i = tp2;  //3

如果有两个线程m和n要执行i++操作,因为重排序的影响,代码执行顺序可能会发生改变。如果代码的执行顺序是m1 - m2 - m3 - n1 - n2 - n3,那么结果是没问题的,如果代码的执行顺序是m1 - n1 - m2 - n2 - m3 - n3那么很明显结果就会出错。

测试代码
package com.wangjun.thread;

public class AtomicIntegerTest {
    
    private static int n = 0;

    public static void main(String[] args) throws InterruptedException {
          //i++引发的线程问题
        Thread t1 = new Thread() {
            public void run() {
                for(int i = 0; i < 1000; i++) {
                    n++;
                }
            }; 
        };
        Thread t2 = new Thread() {
            public void run() {
                for(int i = 0; i < 1000; i++) {
                    n++;
                }
            };
        };
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("最终n的值为:" + n);
    }
}

如果i++是原子操作,那么结果应该就是2000,反复运行几次发现结果大部分情况下都不是2000,这也证明了i++的非原子性在多线程下产生的问题。当然我们可以通过加锁的方式保证操作的原子性,但本文的重点是使用原子类的解决这个问题。

最终n的值为:1367
---
最终n的值为:1243
---
最终n的值为:1380
2. AtomicInteger的原子操作

上面的问题可以使用AtomicInteger来解决,我们更改一下代码如下:

package com.wangjun.thread;

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerTest {
    
    private static AtomicInteger n2 = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
          Thread t1 = new Thread() {
            public void run() {
                for(int i = 0; i < 1000; i++) {
                    n2.incrementAndGet();
                }
            }; 
        };
        Thread t2 = new Thread() {
            public void run() {
                for(int i = 0; i< 1000; i++) {
                    n2.incrementAndGet();
                }
            }
        };
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("最终n2的值为:" + n2.toString());
    }
}

多次运行,发现结果永远是2000,由此可以证明AtomicInteger的操作是原子性的。

最终n2的值为:2000

那么AtomicInteger是通过什么机制来保证原子性的呢?接下来,我们对源码进行一下分析。

3. AtomicInteger源码分析 构造函数
private volatile int value;
/*
 * AtomicInteger内部声明了一个volatile修饰的变量value用来保存实际值
 * 使用带参的构造函数会将入参赋值给value,无参构造器value默认值为0
 */
public AtomicInteger(int initialValue) {
  value = initialValue;
}
自增函数
import sun.misc.Unsafe;
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
  try {
    valueOffset = unsafe.objectFieldOffset
      (AtomicInteger.class.getDeclaredField("value"));
  } catch (Exception ex) { throw new Error(ex); }
}
/*
 * 可以看到自增函数中调用了Unsafe函数的getAndAddInt方法
 */
public final int incrementAndGet() {
  return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

那么这个getAndAddInt方法是干嘛的呢,首先来了解一下Unsafe这个类。

Unsafe类是在sun.misc包下,不属于Java标准。但是很多Java的基础类库,包括一些被广泛使用的高性能开发库都是基于Unsafe类开发的,比如Netty、Cassandra、Hadoop、Kafka等。Unsafe类在提升Java运行效率,增强Java语言底层操作能力方面起了很大的作用。
Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针的问题。过度的使用Unsafe类会使得出错的几率变大,因此Java官方并不建议使用的,官方文档也几乎没有。
通常我们最好也不要使用Unsafe类,除非有明确的目的,并且也要对它有深入的了解才行。

再来说Unsafe的getAndAddInt,通过反编译可以看到实现代码:

/*
 * 其中getIntVolatile和compareAndSwapInt都是native方法
 * getIntVolatile是获取当前的期望值
 * compareAndSwapInt就是我们平时说的CAS(compare and swap),通过比较如果内存区的值没有改变,那么就用新值直接给该内存区赋值
 */
public final int getAndAddInt(Object paramObject, long paramLong, int paramInt)
{
  int i;
  do
  {
    i = getIntVolatile(paramObject, paramLong);
  } while (!compareAndSwapInt(paramObject, paramLong, i, i + paramInt));
  return i;
}

incrementAndGet是将自增后的值返回,还有一个方法getAndIncrement是将自增前的值返回,分别对应++ii++操作。同样的decrementAndGet和getAndDecrement则对--ii--操作。

参考:

java的Unsafe类:https://www.cnblogs.com/pkufo...

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

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

相关文章

  • Java多线程学习(九)JUC 中的 Atomic 原子类总结

    摘要:即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。另外是一个变量,在内存中可见,因此可以保证任何时刻任何线程总能拿到该变量的最新值。 个人觉得这一节掌握基本的使用即可! 本节思维导图: showImg(https://segmentfault.com/img/remote/1460000016855442?w=1244&h=657); 1 Atomic 原子类介绍...

    Youngs 评论0 收藏0
  • 线程池原理浅析

    摘要:线程池主要解决两个问题一是当执行大量异步任务时线程池能够提供很好的性能。二是线程池提供了一种资源限制和管理的手段,比如可以限制现成的个数,动态新增线程等。该方法返回一个对象,可指定线程池线程数量。 什么是线程池? 为了避免频繁重复的创建和销毁线程,我们可以让这些线程进行复用,在线程池中,总会有活跃的线程在占用,但是线程池中也会存在没有占用的线程,这些线程处于空闲状态,当有任务的时候会从...

    未东兴 评论0 收藏0
  • 悲观锁和乐观锁以及CAS机制

    摘要:加锁才能保证线程安全使用之后,不加锁,也是线程安全的。确保不出现线程安全问题。一般在数据库中使用乐观锁都会拿版本号作为对比值,因为版本号会一直增加,没有重复的,所以不会出现这个问题。 悲观锁: 认为每次获取数据的时候数据一定会被人修改,所以它在获取数据的时候会把操作的数据给锁住,这样一来就只有它自己能够操作,其他人都堵塞在那里。 乐观锁: 认为每次获取数据的时候数据不会被别人修改,所以...

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

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

    高胜山 评论0 收藏0
  • AtomicInteger 原子类的作用

    摘要:原子类的作用多线程操作,性能开销太大并不是原子操作。每次比较的是两个对象性能比要好使用时,在高并发下大量线程会同时去竞争更新同一个原子变量,但是由于同时只有一个线程的会成功,所以其他线程会不断尝试自旋尝试操作,这会浪费不少的资源。 AtomicInteger 原子类的作用 多线程操作,Synchronized 性能开销太大count++并不是原子操作。因为count++需要经过读取-...

    MartinDai 评论0 收藏0

发表评论

0条评论

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