Java多线程之Atomic:原子变量与原子类

Atomic简介

​ Atomic包是java.util.concurrent下的另一个专门为线程安全设计的Java包,包含多个原子操作类这个包里面提供了一组原子变量类。

​ 其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个另一个线程进入,这只是一种逻辑上的理解。实际上是借助硬件的相关指令来实现的,不会阻塞线程(或者说只是在硬件级别上阻塞了)。可以对基本数据、数组中的基本数据、对类中的基本数据进行操作。原子变量类相当于一种泛化的volatile变量,能够支持原子的和有条件的读-改-写操作。

传统锁的问题

我们先来看一个例子:计数器(Counter),采用Java里比较方便的锁机制synchronized关键字,初步的代码如下:


class Counter {
		
	private int value;
 
	public synchronized int getValue() {
		return value;
	}
 
	public synchronized int increment() {
		return ++value;
	}
 
	public synchronized int decrement() {
		return --value;
	}
}

其实像这样的锁机制,满足基本的需求是没有问题的了,但是有的时候我们的需求并非这么简单,我们需要更有效,更加灵活的机制,synchronized关键字是基于阻塞的锁机制,也就是说当一个线程拥有锁的时候,访问同一资源的其它线程需要等待,直到该线程释放锁,这里会有些问题:首先,如果被阻塞的线程优先级很高很重要怎么办?其次,如果获得锁的线程一直不释放锁怎么办?(这种情况是非常糟糕的)。还有一种情况,如果有大量的线程来竞争资源,那CPU将会花费大量的时间和资源来处理这些竞争(事实上CPU的主要工作并非这些),同时,还有可能出现一些例如死锁之类的情况,最后,其实锁机制是一种比较粗糙,粒度比较大的机制,相对于像计数器这样的需求有点儿过于笨重,因此,对于这种需求我们期待一种更合适、更高效的线程安全机制,于是CAS诞生了。

传送门:CAS

Atomic

java.util.concurrent.atomic中的类可以分成4组:

  • 标量类(Scalar):AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
  • 数组类:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
  • 更新器类:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
  • 复合变量类:AtomicMarkableReference,AtomicStampedReference

基础数据型

第一组AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference这四种基本类型用来处理布尔,整数,长整数,对象四种数据,其内部实现不是简单的使用synchronized,而是一个更为高效的方式CAS (compare and swap) + volatile和native方法,从而避免了synchronized的高开销,执行效率大为提升。我们来看个例子,与我们平时i++所对应的原子操作为:getAndIncrement()

package com.company.atomics;

/*
* 使用原子变量类定义一个计数器
* 该计数器在整个程序中都能使用,并且在所有的地方都可以使用这个计数器,
* 这个计数器可以设计为单例
* */

import java.util.concurrent.atomic.AtomicLong;

public class Indicator {
    //构造方法私有化
    private Indicator(){}
    //定义一个私有的本类静态的对象
    private static final Indicator INSTANCE=new Indicator();
    //提供一个公共静态方法返回唯一实例
    public static Indicator getInstance(){
        return INSTANCE;
    }
    //使用原子变量类保存请求总数,成功数,失败数
    private final AtomicLong requestCount=new AtomicLong(0);
    private final AtomicLong successCount=new AtomicLong(0);
    private final AtomicLong failureCount=new AtomicLong(0);

    //有新的请求
    public void requestProcessReceive(){
        requestCount.incrementAndGet();
    }
    //处理成功
    public void requestProcessSuccess(){
        successCount.incrementAndGet();
    }
    //处理失败
    public void requestProcessFailur(){
        failureCount.incrementAndGet();
    }
    //查看总数,
    public long getRequestCount(){
        return requestCount.get();
    }
    //查看成功数
    public long getSuccessCount(){
        return successCount.get();
    }
    //查看失败数
    public long getFailurCount(){
        return failureCount.get();
    }
}

结果测试:

package com.company.atomics;
import java.util.Random;
public class IndicatorTest {
    public static void main(String[] args) {
        //通过线程模拟请求
        for (int i = 0; i < 10000; i++) {
         new Thread(new Runnable() {
             @Override
             public void run() {
                 //每个线程就是一个请求,请求总数要+1
                 Indicator.getInstance().requestProcessReceive();
                 int num=new Random().nextInt();
                 if (num%2==0){  //处理成功
                     Indicator.getInstance().requestProcessSuccess();
                 }else{   //处理失败
                     Indicator.getInstance().requestProcessFailur();
                 }
             }
         }).start();
        }

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Indicator.getInstance().getRequestCount());//总数
        System.out.println(Indicator.getInstance().getSuccessCount());//成功数
        System.out.println(Indicator.getInstance().getFailurCount());//失败数

    }
}

常用方法

  • 构造函数(两个构造函数)

    • 默认的构造函数:初始化的数据分别是false,0,0,null
    • 带参构造函数:参数为初始化的数据
  • set( )和get( )方法:可以原子地设定和获取atomic的数据。类似于volatile,保证数据会在主存中设置或读取

  • void set()和void lazySet():set设置为给定值,直接修改原始值;lazySet延时设置变量值,这个等价于set()方法,但是由于字段是volatile类型的,因此次字段的修改会比普通字段(非volatile字段)有稍微的性能延时(尽管可以忽略),所以如果不是想立即读取设置的新值,允许在“后台”修改值,那么此方法就很有用。

  • getAndSet( )方法

    • 原子的将变量设定为新数据,同时返回先前的旧数据

    • 其本质是get( )操作,然后做set( )操作。尽管这2个操作都是atomic,但是他们合并在一起的时候,就不是atomic。在Java的源程序的级别上,如果不依赖synchronized的机制来完成这个工作,是不可能的。只有依靠native方法才可以。

      public final int getAndSet(int newValue) {  
          for (;;) {  
              int current = get();  
              if (compareAndSet(current, newValue))  
                  return current;  
          }  
      }  
      
  • compareAndSet( ) 和weakCompareAndSet( )方法

    这两个方法都是conditional modifier方法。接收2个参数,一个是期望数据(expected),一个是新数据(new),如果atomic里面的数据和期望数据一 致,则将新数据设定给atomic的数据,返回true,表明成功;否则就不设定,并返回false。JSR规范中说:以原子方式读取和有条件地写入变量但不 创建任何 happen-before 排序,因此不提供与除 weakCompareAndSet 目标外任何变量以前或后续读取或写入操作有关的任何保证。大意就是说调用weakCompareAndSet时并不能保证不存在happen- before的发生(也就是可能存在指令重排序导致此操作失败)。但是从Java源码来看,其实此方法并没有实现JSR规范的要求,最后效果和 compareAndSet是等效的,都调用了unsafe.compareAndSwapInt()完成操作。

    public final boolean compareAndSet(int expect, int update) {  
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);  
    }  
    public final boolean weakCompareAndSet(int expect, int update) {  
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);  
    }  
    
  • 对于 AtomicInteger、AtomicLong还提供了一些特别的方法。
    getAndIncrement( ):以原子方式将当前值加 1,相当于线程安全的i++操作。
    incrementAndGet( ):以原子方式将当前值加 1, 相当于线程安全的++i操作。
    getAndDecrement( ):以原子方式将当前值减 1, 相当于线程安全的i--操作。
    decrementAndGet ( ):以原子方式将当前值减 1,相当于线程安全的--i操作。
    addAndGet( ): 以原子方式将给定值与当前值相加, 实际上就是等于线程安全的i =i+delta操作。
    getAndAdd( ):以原子方式将给定值与当前值相加, 相当于线程安全的t=i;i+=delta;return t;操作。
    以实现一些加法,减法原子操作。(注意 --i、++i不是原子操作,其中包含有3个操作步骤:第一步,读取i;第二步,加1或减1;第三步:写回内存)

数组型

AtomicIntegerArray,AtomicLongArray还有AtomicReferenceArray类进一步扩展了原子操作,对这些类型的数组提供了支持。这些类在为其数组元素提供 volatile 访问语义方面也引人注目,这对于普通数组来说是不受支持的。

他们内部并不是像AtomicInteger一样维持一个valatile变量,而是全部由native方法实现,如下
AtomicIntegerArray的实现片断:

private static final Unsafe unsafe = Unsafe.getUnsafe();  
private static final int base = unsafe.arrayBaseOffset(int[].class);  
private static final int scale = unsafe.arrayIndexScale(int[].class);  
private final int[] array;  
public final int get(int i) {  
        return unsafe.getIntVolatile(array, rawIndex(i));  
}  
public final void set(int i, int newValue) {  
        unsafe.putIntVolatile(array, rawIndex(i), newValue);  
}  

基本使用示例:

public class AtomicIntegerArrayTest {
    public static void main(String[] args) {
        //创建一个指定长度的原子数组
        AtomicIntegerArray atomicIntegerArray=new AtomicIntegerArray(10);
        System.out.println(atomicIntegerArray);//[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
        //返回指定位置的元素
        System.out.println(atomicIntegerArray.get(0));//0
        System.out.println(atomicIntegerArray.get(1));//0
        //设置指定位置的元素
        atomicIntegerArray.set(0,10);
        //先获取指定位置的值在设置
        System.out.println(atomicIntegerArray.getAndSet(1,11));//0
        //把数组元素加上某个值
        System.out.println(atomicIntegerArray.addAndGet(0,22));//32
        System.out.println(atomicIntegerArray.getAndAdd(1,33));//11
        System.out.println(atomicIntegerArray);//[32, 44, 0, 0, 0, 0, 0, 0, 0, 0]

        //CAS操作,如果0位置的值是32,就将其值修改为222
        System.out.println(atomicIntegerArray.compareAndSet(0,32,222));//返回ture
        //先自增在返回
        System.out.println(atomicIntegerArray.incrementAndGet(0));//223
        //先返回再自减
        System.out.println(atomicIntegerArray.getAndIncrement(1));//44
        System.out.println(atomicIntegerArray);//[223, 45, 0, 0, 0, 0, 0, 0, 0, 0]
        //先自减再返回
        System.out.println(atomicIntegerArray.decrementAndGet(2));//-1
        //先返回再自减
        System.out.println(atomicIntegerArray.getAndDecrement(3));//0
        System.out.println(atomicIntegerArray);//[223, 45, -1, -1, 0, 0, 0, 0, 0, 0]

    }
}

import java.util.concurrent.atomic.AtomicIntegerArray;

public class AtomicIntegerArrayTest01 {
    //定义一个原子数组
    static AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(10);

    public static void main(String[] args) {
        //定义线程数组
        Thread[] threads=new Thread[10];
        //给线程数组赋值
        for (int i = 0; i < threads.length; i++) {
            threads[i]=new addThread();
        }
        //开启子线程
        for (Thread thread:threads
             ) {
            thread.start();
        }

        //在主线程中查看自增完后原子数组中各个元素的值,主线程中需要在所有子线程都执行完后查看
        //把所有的子线程合并到当前主线程中
        for (Thread thread:threads
             ) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
         System.out.println(atomicIntegerArray)
    }

    //定义一个线程类,在线程类中修改原子数组
    static class addThread extends Thread{
        @Override
        public void run() {
            //把数组元素的每个元素自增1000次
            for (int i = 0; i < 1000; i++) {
                for (int j = 0; j < atomicIntegerArray.length() ; j++) {
                    atomicIntegerArray.getAndIncrement(i%atomicIntegerArray.length());
                }
            }
        }
    }
}

输出结果:

//10个线程,每个线程自增1000次
[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]

字段更新器

AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater基于反射的实用工具,可以对指定类的指定 volatile 字段进行原子更新。API非常简单,但是也是有一些约束:

(1)字段必须是volatile类型的

(2)字段的描述类型(修饰符public/protected/default/private)是与调用者与操作对象字段的关系一致。也就是说 调用者能够直接操作对象字段,那么就可以反射进行原子操作。但是对于父类的字段,子类是不能直接操作的,尽管子类可以访问父类的字段。

(3)只能是实例变量,不能是类变量,也就是说不能加static关键字。

(4)只能是可修改变量,不能使final变量,因为final的语义就是不可修改。实际上final的语义和volatile是有冲突的,这两个关键字不能同时存在。

(5)对于AtomicIntegerFieldUpdaterAtomicLongFieldUpdater 只能修改int/long类型的字段,不能修改其包装类型(Integer/Long)。如果要修改包装类型就需要使用AtomicReferenceFieldUpdater

netty5.0中类ChannelOutboundBuffer统计发送的字节总数,由于使用volatile变量已经不能满足,所以使用AtomicIntegerFieldUpdater 来实现的

示例:

//定义一个User实体类
public class User {
     int id;

     volatile int age;

    public User(int id, int age) {
        this.id = id;
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", age=" + age +
                '}';
    }
}

//定义一个线程类,实现字段自增
public class AtomicUpdater extends Thread{
    private User user;

    private AtomicIntegerFieldUpdater<User> updater = AtomicIntegerFieldUpdater.newUpdater(User.class,"age");

    public AtomicUpdater(User user) {
        this.user = user;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(updater.getAndIncrement(user));
        }
    }
}

//测试类
public class Test {
    public static void main(String[] args) {
        //初始化User对象
        User user = new User(1, 10);
        //开启10个线程
        for (int i = 0; i < 10; i++) {
            new AtomicUpdater(user).start();
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(user);
    }
}

输出结果:

//一个线程自增10次,10个线程自增100次
User{id=1, age=110}

引用型

原子更新基本类型的AtomicInteger,只能更新一个变量,如果要原子更新多个变量,就需要使用这个原子更新引用类型提供的类。Atomic包提供了以下3个类

AtomicReference

import java.util.concurrent.atomic.AtomicReference;

public class AtomicReferenceTest {
    static AtomicReference<String> atomicReference=new AtomicReference<>("abc");

    public static void main(String[] args) {
        //创建十个线程修改字符串
        for (int i = 0; i < 100; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    if (atomicReference.compareAndSet("abc","def")){
                        System.out.println(Thread.currentThread().getName()+"把字符串修改为了def");
                    }
                }
            }).start();
        }
        //再创建100个线程
        for (int i = 0; i < 100; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    if (atomicReference.compareAndSet("def","abc")){
                        System.out.println(Thread.currentThread().getName()+"把字符串还原为了abc");
                    }
                }
            }).start();
        }

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(atomicReference.get());
    }

}

AtomicStampedReference

由于atomicReference是采用了CAS,所以可能会产生ABA问题。利用版本戳的形式记录了每次改变以后的版本号,这样的话就不会存在ABA问题了。这就是AtomicStampedReference的解决方案。

public class AtomicStampReferenceTest {

    //定义AtomicStampedReference引用操作"abc"字符串,指定初始版本号为0
    private static AtomicStampedReference<String> stampedReference=new AtomicStampedReference<>("abc",0);

    public static void main(String[] args) {
        Thread t1=new Thread(new Runnable() {
            @Override
            public void run() {
   stampedReference.compareAndSet("abc","def",
                                  stampedReference.getStamp(),
                                  stampedReference.getStamp()+1);
                
System.out.println(Thread.currentThread().getName()+"----"+stampedReference.getReference());
                
                stampedReference.compareAndSet("def","abc",
                                               stampedReference.getStamp(),
                                               stampedReference.getStamp()+1);
            }
        });
        Thread t2=new Thread(new Runnable() {
            //如果在此处获取stamp,则可能无法获取当前的版本号,在线程睡眠的那一秒中,版本号可能发生了改变
            //int stamp=stampedReference.getStamp();//获取版本号
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                int stamp=stampedReference.getStamp();//获取版本号
                System.out.println(stampedReference.compareAndSet("abc","ggg",stamp,stamp+1));
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(stampedReference.getReference());

    }
}

AtomicMarkableReference

原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef,booleaninitialMark)

posted @ 2021-05-23 14:57  至安  阅读(990)  评论(0编辑  收藏  举报