线程安全之原子操作

什么是原子操作

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

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

下面我们看一段代码:

package com.hzw;

public class Counter {
    volatile int i = 0;

    public void add() {
        i++;
    }
}
package com.hzw;

public class DemoCounterTest {
    public static void main(String[] args) 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);
    }
}

预计打印i的结果应该是10X10000但是实际并不是,打印的结果要比预计的小,这是为什么呢?

没错,出现了线程安全问题,因为i++不是原子操作导致的线程安全问题。

下面我们来剖析一下,为什么i++不是原子性操作

通过javap -v -p Counter.class对Counter文件进行反编译,查看add方法编译后的字节码指令
在这里插入图片描述
可知,如上图,i++方法被编译成了四个字节码指令。那么这四个字节码指令是如何执行的呢?在这里插入图片描述

  • getfield:从堆内存中获取字段的值放入操作数栈
  • Icons_1:将常量1放入操作数栈
  • iadd:从操作数栈顶取出两个值求和以后再压入操作数栈
  • putfield:将操作数栈里的数据去除设回到堆内存中的字段

当多个线程同时操作时线程1与线程2同时拿到i=0当线程1完成putfield后堆内存里``i=1与线程2拿到的i=0不一致,不符合原子性的特征,所以i++`不是原子性操作
在这里插入图片描述

原子性问题的场景

  • 判断了某种状态后,这个状态失效了

    if(owner == null) {
    		owner = currentThread();
    }
    

    当线程1与线程2同时进入if代码块,结果线程1将owner进行赋值,此时线程2进入if代码块的判断状态owner == null失效了因为此时owner != null,造成了原子性问题

  • 加载了一个值,这个值失效了
    例如:上文中多个线程之间的i++操作

如何解决

  • 对需要保证原子操作的代码进行加锁

    package com.hzw;
    
    public class CounterSync {
     volatile int i = 0;
    
     public synchronized void add() {
         i++;
     }
    }
    
    package com.hzw;
    
    public class CounterLock {
     volatile int i = 0;
    
     Lock lock = new ReentrantLock();
     public void add() {
       lock.lock();
       try {
         i++;
       }finally {
         lock.unlock();
       }
     }
    }
    
  • 使用AtomicInteger类进行原子性的i++操作

    package com.hzw;
    
    public class CounterAtomic {
     AtomicInteger i = new AtomicInteger(0);
    
     public void add() {
         i++;
     }
    }
    

    在AtomicInteger类中使用了CAS操作实现了原子性的i++操作,那么什么是CAS操作呢?

CAS(Compare and swap)

Compare and swap 比较和交换。属于硬件同步原语,处理器提供了基本内存操作的原子性保证,即CAS是一个原子操作。

CAS操作需要输入两个数值,一个旧值A(期望操作前的值)和一个新值B。在操作期间先对旧值进行比较,若没有发生变化,才交换成新值,发生了变化则不交换,此时会在次重新取旧值进行CAS操作,形成一个自旋。
在这里插入图片描述

使用CAS解决原子性问题

Java中的sun.misc.Unsafe类,提供了compareAndSwapInt()和compareAndSewapLong()等几个方法实现CAS。

package com.hzw.subject1;

import sun.misc.Unsafe;

import java.lang.reflect.Field;

public class CounterUnsafe {
    volatile int i = 0;

    private static Unsafe unsafe = null;

    //i字段的偏移量
    private static long valueOffset;

    static {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
            //获取i字段的偏移量
            Field fieldI = CounterUnsafe.class.getDeclaredField("i");
            valueOffset = unsafe.objectFieldOffset(fieldI);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    public void add() {
        for (; ; ) {
            int current = unsafe.getIntVolatile(this, valueOffset);
            if (unsafe.compareAndSwapInt(this, valueOffset, current, current + 1)) {
                break;
            }
        }
    }
}

什么是字段偏移量?
在这里插入图片描述
在对象中除了可以通过普通get/set方法或反射操作字段外,还可以通过字段偏移量进行操作字段,如上图,如果要访问age字段,就可以通过字段偏移量10进行访问

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值