Java并发编程—Atomic原子类

目录

Atomic

1. AtomicInteger

a. 多线程并发访问问题

b. 用 AtomicInteger 类解决

2. AtomicIntegerArray

a. 多线程并发访问问题

b. 用 AtomicIntegerArray 类解决

相关问题


Atomic

在 java.util.concurrent.atomic 包下定义了一些对“变量”操作的“原子类”,它们可以保证对“变量”操作的:原子性、有序性、可见性:

1. AtomicInteger

a. 多线程并发访问问题

class MyThread extends Thread {
    public static volatile int a = 0;

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            //线程1:取出a的值a=0(被暂停)
            a++;
            //写回
        }
        System.out.println("修改完毕!");
    }
}

public class Test {
    public static void main(String[] args) throws InterruptedException {
        //1.启动两个线程
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        t1.start();
        t2.start();
        Thread.sleep(1000);
        System.out.println("获取a最终值:" + MyThread.a);//最终结果仍然不正确。
    }
}
/*
输出
修改完毕!
修改完毕!
获取a最终值:14791
 */
  • 加入 volatile 关键字并不能解决变量自增操作的原子性问题;

b. 用 AtomicInteger 类解决

  • 用 AtomicInteger 类解决了多线程的原子性问题,无论程序运行多少次,其结果总是正确的;
import java.util.concurrent.atomic.AtomicInteger;

class MyThread extends Thread {
    //public static volatile int a = 0;//不直接使用基本类型变量
    //改用"原子类"
    public static AtomicInteger a = new AtomicInteger(0);

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            // a++;
            a.getAndIncrement();//先获取,再自增1:a++
        }
        System.out.println("修改完毕!");
    }
}

public class Test {
    public static void main(String[] args) throws InterruptedException {
        //1.启动两个线程
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        t1.start();
        t2.start();
        Thread.sleep(1000);
        System.out.println("获取a最终值:" + MyThread.a.get());
    }
}
/*
输出
修改完毕!
修改完毕!
获取a最终值:20000
 */

c. 工作原理:CAS 机制

在 Unsafe 类中,调用了一个:compareAndSwapInt() 方法,此方法的几个参数:

  • var1:传入的 AtomicInteger 对象
  • var2:AtommicInteger 内部变量的偏移地址
  • var5:之前取出的 AtomicInteger 中的值
  • var5 + var4:预期结果

此方法使用了一种"比较并交换(Compare And Swap)"的机制,它会用 var1 和 var2 先获取内存中 AtomicInteger 中的值,然后和传入的,之前获取的值 var5 做一下比较,也就是比较当前内存的值和预期的值是否一致,如果一致就修改为 var5 + var4,否则就继续循环,再次获取 AtomicInteger 中的值,再进行比较并交换,直至成功交换为止;compareAndSwapInt() 方法是"线程安全"的;我们假设两个线程交替运行的情况,看看它是怎样工作的:

  • 初始 AtomicInteger 的值为0
  • 线程A执行:var5 = this.getIntVolatile(var1,var2);获取的结果为:0
  • 线程A被暂停
  • 线程B执行:var5 = this.getIntVolatile(var1,var2);获取的结果为:0
  • 线程B执行:this.compareAndSwapInt(var1,var2,var5,var5 + var4)
  • 线程B成功将 AtomicInteger 中的值改为1
  • 线程A恢复运行,执行:this.compareAndSwapInt(var1,var2,var5,var5 + var4)
  • 此时线程A使用 var1 和 var2 从 AtomicInteger 中获取的值为:1,而传入的 var5 为0,比较失败,返回 false,继续循环
  • 线程A执行:var5 = this.getIntVolatile(var1,var2);获取的结果为:1
  • 线程A执行:this.compareAndSwapInt(var1,var2,var5,var5 + var4)
  • 此时线程A使用 var1 和 var2 从 AtomicInteger 中获取的值为:1,而传入的var5为1,比较成功,将其修改为 var5 + var4,也就是2,将 AtomicInteger 中的值改为2,结束;

CAS 机制也被称为乐观锁机制、自旋锁机制,因为大部分比较的结果为 true,就直接修改了。只有少部分多线程并发的情况会导致 CAS 失败,而再次循环;

CAS

2. AtomicIntegerArray

常用的数组操作的原子类,它们可以解决数组的多线程并发访问的安全性问题:

a. 多线程并发访问问题

class MyThread extends Thread {
    private static int[] intArray = new int[1000];//不直接使用数组

    @Override
    public void run() {
        for (int i = 0; i < getIntArrayLength(); i++) {
            intArray[i]++;
        }
    }

    public static int getIntArray(int i) {
        return intArray[i];
    }

    public static int getIntArrayLength() {
        return intArray.length;
    }
}

public class Test {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            new MyThread().start();//创建1000个线程,每个线程为数组的每个元素+1
        }
        Thread.sleep(1000 * 5);//让所有线程执行完毕
        System.out.println("主线程休息5秒醒来");
        for (int i = 0; i < MyThread.getIntArrayLength(); i++) {
            System.out.println(MyThread.getIntArray(i));
        }
    }
}
/*
部分输出
...
999
999
1000
1000
...
 */
  • 可以发现,有些元素并不是1000;

b. 用 AtomicIntegerArray 类解决

  • AtomicIntegerArray 类可以保证数组的多线程安全;
import java.util.concurrent.atomic.AtomicIntegerArray;

class MyThread extends Thread {
    private static int[] intArray = new int[1000];//定义一个数组
    //改用原子类,使用数组构造
    public static AtomicIntegerArray arr = new AtomicIntegerArray(intArray);

    @Override
    public void run() {
        for (int i = 0; i < arr.length(); i++) {
            arr.addAndGet(i, 1);//将i位置上的元素+1
        }
    }
}

public class Test {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            new MyThread().start();
        }
        Thread.sleep(1000 * 5);//让所有线程执行完毕
        System.out.println("主线程休息5秒醒来");
        for (int i = 0; i < MyThread.arr.length(); i++) {
            System.out.println(MyThread.arr.get(i));
        }
    }
}
/*
输出
1000
1000
1000
1000
...
 */
  • 可见,每次运行的结果都是正确的;

相关问题

1:为什么会出现Atomic类

  在多线程或者并发环境中,我们常常会遇到这种情况 int i=0; i++ 稍有经验的同学都知道这种写法是线程不安全的。为了达到线程安全的目的,我们通常会用synchronized来修饰对应的代码块。现在我们有了新的方法,就是使用J.U.C包下的atomic类。

2:Atomic类的原理是什么呢

一句话来说,atomic类是通过自旋CAS操作volatile变量实现的。CAS是compare and swap的缩写,即比较后(比较内存中的旧值与预期值)交换(将旧值替换成预期值)。它是sun.misc包下Unsafe类提供的功能,需要底层硬件指令集的支撑。使用volatile变量是为了多个线程间变量的值能及时同步。

3:为什么使用Atomic类

按理来说,使用synchroized已经能满足功能需求了。为什么还会有这个类呢?那肯定是性能的问题了。在JDK1.6之前,synchroized是重量级锁,即操作被锁的变量前就对对象加锁,不管此对象会不会产生资源竞争。这属于悲观锁的一种实现方式。而CAS会比较内存中对象和当前对象的值是否相同,相同的话才会更新内存中的值,不同的话便会返回失败。这是乐观锁的一中实现方式。这种方式就避免了直接使用内核状态的重量级锁。但是在JDK1.6以后,synchronized进行了优化,引入了偏向锁,轻量级锁,其中也采用了CAS这种思想,效率有了很大的提升。

4:Atomic类的缺点

1)ABA问题:对于一个旧的变量值A,线程2将A的值改成B又改成可A,此时线程1通过CAS看到A并没有变化,但实际A已经发生了变化,这就是ABA问题。解决这个问题的方法很简单,记录一下变量的版本就可以了,在变量的值发生变化时对应的版本也做出相应的变化,然后CAS操作时比较一下版本就知道变量有没有发生变化。atomic包下AtomicStampedReference类实现了这种思路。Mysql中Innodb的多版本并发锁也是这个原理

2)自旋问题:atomic类会多次尝试CAS操作直至成功或失败,这个过程叫做自旋。通过自旋的过程我们可以看出自旋操作不会将线程挂起,从而避免了内核线程切换,但是自旋的过程也可以看做CPU死循环,会一直占用CPU资源。这种情形在单CPU的机器上是不能容忍的,因此自旋一般都会有个次数限制,即超过这个次数后线程就会放弃时间片,等待下次机会。因此自旋操作在资源竞争不激烈的情况下确实能提高效率,但是在资源竞争特别激烈的场景中,CAS操作会的失败率就会大大提高,这时使用中重量级锁的效率可能会更高。当前,也可以使用LongAdder类来替换,它则采用了分段锁的思想来解决并发竞争的问题。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值