线程安全之原子操作

被final修饰的变量会返回正确的构造版本,在一定程度上(构造函数)可以解决内存可见性问题,如下代码:

package 多线程;

import org.junit.Test;

/**
 * @Author: cxl
 * @Date: 2020/3/3 17:05
 * @Version 1.0
 */
public class Test01 {

    final int x;
    int y;
    int z;
    static Test01 t;

    public Test01() {
        x = 3;
        y = 4;
//        z = x;   因为x被final修饰了,所以也可以读到z的正确构造版本
    }

    public static void main(String[] args){
        new Thread(new Runnable() {
            @Override
            public void run() {
               t = new Test01();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                if (t != null) {
                    System.out.println(t.x + ">>>" +t.y);
                }
            }
        }).start();
    }
}

正儿八经的原子操作问题看下图:

package 多线程;

import org.junit.Test;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @Author: cxl
 * @Date: 2020/3/10 17:21
 * @Version 1.0
 */
public class Test02 {

    public class Counter{
        volatile int i = 0;

        public void add() {
            i ++;
        }
    }

    @Test
    public void test1() throws InterruptedException {
       final Counter counter = new Counter();
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 10000; j++) {
                        counter.add();
                    }
                    System.out.println("done...");
                }
            }).start();
        }
        Thread.sleep(6000L);
        System.out.println(counter.i);
    }
}

看这个代码,结果是100000,可是一运行,结果却不是我们想的那样:

what?我的工具出了问题?多运行几次发现每次的结果都不一样,69696、54815...,猜测可能是计算变量i那块的代码出了问题,i++并不是一个原子操作,通过对Counter类反编译,查看对应的字节码指令:

public void add();
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #3                  // Field i:I
         5: iconst_1
         6: iadd
         7: putfield      #3                  // Field i:I
        10: return
      LineNumberTable:
        line 20: 0
        line 21: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0      11     0  this   L多线程/Test02$Counter;
}

这个i++操作是由0~10步组合完成的,要搞清楚i++操作是如何出问题的,我们就要知道这几步是什么意思,咱们逐步分析:

首先:

然后:

第三步:

最后一步:

当引入多线程时:

t1和t2都加载了i的值0(t1先,t2后),t1到红线的位置执行完i++操作将1写入堆内存中,这时t2线程在执行iadd操作的,他读取到的i的值仍然为0不是t1回写到的1,所以t2写到堆内存的值还是1,t2加载的值就失效了(没有加载到t1的1值),这四个操作被分割了,不是原子操作,这也是上面的代码结果远少于100000的问题原因。

原子操作:原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分(不可中断性)。

将整个操作视作一个整体,资源在该此操作中保持一致,这是原子性的核心特征。

我们这个例子就是典型的i的值被分割了,资源没有保持一致,不是一个原子操作。

一般出现原子性的两种典型场景

1、判断的某种状态后,这个状态失效了

if(owner == null){
    owner = CurrentThread()
}

当多个线程执行这块代码,线程1判空成功进行赋值操作的一瞬间,别的线程在进行判空的时候,owner就失效了

2、加载了一个值,这个值失效了,上面示图的情况

那么,该怎么解决这个问题呢,其实在三种办法:

1、可以在add方法上加锁,add方法一次只能有一个线程执行,原子性得到了保证,:

        public synchronized void add() {
            i ++;
        }

2、原理同1

        Lock lock = new ReentrantLock();
        public void add() {
            lock.lock();
            try {
                i ++;
            } finally {
                lock.unlock();
            }
        }

3、使用原子操作类

        AtomicInteger i = new AtomicInteger(0);
        public void add() {
            i.getAndIncrement();
        }

方法1和2做法都是将方法阻塞,多线程变成单线程执行操作,那么AtomicInteger 又有什么不同呢?其实,这里就要引入一个CAS的概念,硬件级别的原语,cpu用于对内存进行修改:

    在计算机科学中,比较和交换(Conmpare And Swap)是用于实现多线程同步的原子指令。 它将内存位置的内容与给定值进行比较,只有在相同的情况下,将该内存位置的内容修改为新的给定值。 这是作为单个原子操作完成的。 原子性保证新值基于最新信息计算; 如果该值在同一时间被另一个线程更新,则写入将失败。 操作结果必须说明是否进行替换; 这可以通过一个简单的布尔响应(这个变体通常称为比较和设置),或通过返回从内存位置读取的值来完成(摘自维基本科)

cas有两个值,旧值和新值,当旧值和内存条中的值一致时(conmpare)才会进行交换(swap)操作,否则失败,不执行交换操作,重新取内存条中的值循环操作,直到执行交换,而内存条又从硬件级别上限制了同一时刻,只有一个线程能对值进行修改(硬件级别的保护)。

那么,怎么在java里面使用cas操作呢:
package 多线程;

import org.junit.Test;
import sun.misc.Unsafe;

import java.lang.reflect.Field;

/**
 * @Author: cxl
 * @Date: 2020/3/10 22:09
 * @Version 1.0
 */
public class CounterAtomic {
    public int i = 0;
    private static Unsafe unsafe = null;
    private static long valueOffset;

    static {
        //java不允许我们这样直接使用,会抛出一个安全异常,所以用反射来拿到
//        unsafe = Unsafe.getUnsafe();
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe)field.get(null);

            //获取字段i的偏移量
            Field iField = CounterAtomic.class.getDeclaredField("i");
            valueOffset = unsafe.objectFieldOffset(iField);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }


    public void add() throws NoSuchFieldException {
        int current;
        do {
            //拿到原值
            current = unsafe.getIntVolatile(this, valueOffset);
            //compareAndSwapInt将拿到的原值和内存条中的值做比较,如果不一致,返回false,继续自旋
        }while (!unsafe.compareAndSwapInt(this, valueOffset, current, current+1));
    }

}

再将Counter类改为CounterAtomic类进行测试,结果正常 

package 多线程;

import org.junit.Test;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author: cxl
 * @Date: 2020/3/10 17:21
 * @Version 1.0
 */
public class Test02 {

    @Test
    public void test1() throws InterruptedException {
       final CounterAtomic counter = new CounterAtomic();
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 10000; j++) {
                        try {
                            counter.add();
                        } catch (NoSuchFieldException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println("done");
                }
            }).start();
        }
        Thread.sleep(1000L);
        System.out.println(counter.i);
    }
}

 

其实JAVA1.5开始引入了CAS,主要代码都放在JUC的atomic包下,AtomicInteger属于其中一个类,getAndIncrement方法实现和我们差不多,这就是第三种和前两种的区别。
 
PS:在jdk1.8lamda表达式中,使用foreach遍历操作元素时,我们常常要设置一个标志位来判断某种条件是否成立来跳出循环,这时定义普通的boolean变量是不行的,
这是因为lamda表达式会额外开启另一个线程来计算,主线程和开启的子线程要保持可见性,提示建议我们可以使用final,但是我们如果要改变可以使用atomicBoolean:
这也解释了atomic可以保持线程之间的可见性(不需要再被volatile修饰)和CAS保持一致性的原因。
 
 
 
总结:多线程下的失去原子性操作大多是因为资源在整个操作中被分割了,无法保持一致,导致其他线程加载的值失效了,写出的也就相当于脏数据,解决办法是加锁阻塞线程,一个执行完后才能给下个线程执行,Atomic下的工具类可以提供给我们更高效和便捷的实现效果,其原理是CAS利用了硬件级别的保护措施,同一时刻只有一个线程写入操作,交换失败自动自旋。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值