高性能编程——线程安全问题之原子操作

原子性问题

其实多线程就只有两个重要的问题,一个是可见性问题,另一个则是原子性问题了。本章将会着重讲原子性问题。

原子操作

上面说到了的原子性问题究竟是什么?先看两段代码和输出结果就知道了。

public class Counter {
    volatile int i = 0 ;

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

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        for(int i=0;i<10;i++){
            new Thread(()->{
               for(int j=0;j<10000;j++){
                   counter.add();
               }
               System.out.println("done...");
            }).start();
        }
        Thread.sleep(6000l);
        System.out.println(counter.i);
    }
}

看了上述代码,相信很多人认为输出结果应该是100000,那么结果究竟是什么?来看:
在这里插入图片描述
wtf?为什么是87690啊,我的i明明加了volatile不应该有可见性问题啊??这就是哟啊引出的原子性问题了。

首先我们来看Counter的add方法:

public void add(){
        i++;
    }

这段代码乍看之下非常短,似乎只有一个操作。然而它正的就只有一步操作么?其实不然,我们只要通过反编译就能看到它真实的执行步骤了。用javap命令反编译一下即可。

javac Counter.java
javap -v -p Counter.class

这样就能得到我们想看的反编译后的字节码了:

 public void add();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field i:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field i:I
        10: return
      LineNumberTable:
        line 7: 0
        line 8: 10

可以看到i++其实就是2567四步所执行的。

问题产生的原因

我们主要来看上述代码2567,这其实又要扯到Java的内存模型了,上面代码的过程其实如下图:
在这里插入图片描述
一个线程会有自己的一块工作空间(栈内存),以及所有线程共享的堆内存,还有共享的方法区。上面这段代码首先是从堆内存里读取i的值,再从栈内存取出1,然后iadd就是做一次运算0+1;最后的putfiled就是把i再放回堆内存。因此可以看出i++并非不可分割,一步到位堆,所以就会有原子性问题。因为当线程多的时候就会造成数据不能实时同步。

原子操作的定义

通过上面的案例可以给出原子性的定义:
原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分(不可中断性)。将整个操作视为一个整体,资源再该操作中保持一致,这是原子性的核心特征。

原子性问题解决方案

如何通过Java中的语言特性和api来解决问题呢?有以下集中方案:

synchronized

public class CounterLock {
    volatile int i = 0;

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

synchronized关键字的作用相当于将代码块用synchronized(this)给同步了,下一章会详细讲解。

ReentranLock

public class CounterLock {
    volatile int i = 0;
    Lock lock = new ReentrantLock();
    public void add(){
        lock.lock();
        try{
            i++;
        }finally {
            lock.unlock();
        }
    }
}

加锁解锁操作,切记释放锁一定要再finally中进行,以防死锁。

AtomicInteger

上面两种写法本质上都是加锁,使得一个线程可以以单线程的方式完全执行,显然效率是偏低的,而AtomicInteger则不用加锁,其原理接下来详细讲述,先来看他的使用:

public class CounterLock {
    AtomicInteger i = new AtomicInteger(0);
    public void add(){
        i.incrementAndGet();
    }
}

再来看看输出:
在这里插入图片描述
可见也实现了原子性。

CAS(Compare and swap)

上文中使用了一个AtomicInteger类,我们就想,如果我们自己想实现一个原子操作应该怎么办。这时候就要用到CAS操作了。

什么是CAS操作

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

CAS操作需要输入两个数值,一个旧值A(期望操作前的值)和一个新值B,在操作期间先对旧值比较,若没有发生变化,才交换成新值,发生了变化则不交换。

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

CAS是在硬件层面上保证了同一时间只有一个线程可以访问内存,而失败的操作宁可不要了也比错了要好。

Java中的CAS操作使用

Java不能直接通过内存地址去操作内存,所以需要通过Unsafe类调用JVM去调用操作系统,如图:
在这里插入图片描述

Unsafe的使用及坑

首先我根据Unsafe的API去获取Unsafe,结果竟然报错,来看一下代码:

public class CounterUnsafe {
    volatile int i = 0;

    private static Unsafe unsafe = null;

    static {
        unsafe = Unsafe.getUnsafe();
    }

    public void add(){

    }

    public static void main(String[] args){
        
    }
}

执行了一下上面这段代码,结果竟然报了这么一个错误:
在这里插入图片描述
这是因为Unsafe.getUnsafe这个API只能是JDK源码才能使用,而你这里不能使用,否则会有安全问题(SecurityException)。。。所以为了使用它,只能通过反射的方式去实现。

再次之前,先要了解一个概念:偏移量

偏移量

偏移量就相当于你想修改内存中的一个字段,但是这时候肯定是不能直接告诉系统该字段再哪的,这时候就要指定偏移量,这时候内存就会移动指定的偏移量到达目标字段的地址了。看图:
在这里插入图片描述
假设绿色的是内存,如果你想要获取j字段,这时候你就要指定到b的末尾作为偏移量,这时候就能读取j字段了。

我们将上述代码改一下:

public class CounterUnsafe {
    volatile int i = 0;

    private static Unsafe unsafe = null;
    private static long valueOffset;

    static {
        //unsafe = Unsafe.getUnsafe();
        try {
            //通过反射获取theUnsafe这个参数;
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);

            //获取i字段的offset
            Field iField = CounterUnsafe.class.getDeclaredField("i");
            valueOffset = unsafe.objectFieldOffset(iField);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    public void add(){
        for(;;) {
            //获取当前对象的valueOffset偏移量处的数(i)
            int current = unsafe.getIntVolatile(this, valueOffset);
            //如果成功了则退出循环,不成功则会继续去执行
            if (unsafe.compareAndSwapInt(this, valueOffset, current, current + 1))
                break;
        }
    }
}

再去试验一下,得到了结果:
在这里插入图片描述
可见已经通过CAS实现了原子性了

J.U.C包内的原子操作封装类

除了上文中的AtomicInteger之外,JUC还提供了很多原子操作的封装类:
在这里插入图片描述

通过API使类的赋予成员变量原子性

我们上述已经自己手写实现了具有原子性的Integer,但是如果实际开发中已经写好了类,这时候想让它具有原子性该如何实现呢?有以下方法:

AtomicIntegerFieldUpdate

public class Demo2_AtomicIntegerFieldUpdate {
    private static AtomicIntegerFieldUpdater<User> atom =
            AtomicIntegerFieldUpdater.newUpdater(User.class,"id");

    public static void main(String[] args) {
        User user = new User(100,100,"Tong");
        atom.addAndGet(user,50);
        System.out.println("addAndGet(user,50)      调整后值变为:"+user);
    }
}

class User{
    volatile int id;
    volatile int age;

    private String name;

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

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

只需要在AtomicIntegerFieldUpdater.newUpdater中指定要执行原子修改的类的Class类以及要执行原子修改的字段即可。

CAS的三个问题

1.CPU消耗过大

循环+CAS,自旋的实现让所有线程都处于高频运行,争抢CPU执行时间的状态。如果操作长时间不成功,会带来很大的CPU资源消耗。

2.只能针对单变量

仅针对单个变量的操作,不能够用于多个变量来实现原子操作

3.ABA问题

在这里插入图片描述
线程1和线程2都要进行CAS(0,1)操作,本来线程1执行完之后轮到线程2执行的时候预期应该是失败的,但是最后因为在它的CAS(0,1)执行之前线程1执行了一次CAS(1,0),所以最终i又变成了0,导致原来了预期失败的CAS(0,1)执行成功了,这样就带来了安全性问题。

ABA问题的解决方案

因为上述问题的主要成因即是i已经不再是原来的那个i了,所以只需要一个能够识别其身份的版本号即可加以区分。

用CAS来实现一个基本的锁

思想解析

,其实就是一个原子引用AtomicRefrence owner,通过它的CAS操作使得当值为null的时候讲它设置为当前线程,表明锁为该线程所占用,而释放锁的时候只要再让参数变为null即可。
其实就是一个原子引用AtomicRefrence<Thread> owner,通过它的CAS操作使得当值为null的时候讲它设置为当前线程,表明锁为该线程所占用,而失败的线程则让它挂起,而释放锁的时候只要再让参数变为null即可。

代码:

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.LockSupport;

/**
 * 个人锁实现
 */
public class MyzLock implements Lock {
    //owner参数用于通过CAS操作来实现原子性
    private AtomicReference<Thread> owner = new AtomicReference<>();
    //等待队列
    private LinkedBlockingQueue<Thread> waiters = new LinkedBlockingQueue();

    @Override
    public void lock() {
        if(!tryLock()){
            //假如加锁失败,则进入等待队列
            waiters.add(Thread.currentThread());
            //用死循环防止伪唤醒问题
            for(;;){
                Thread head = waiters.peek();//读取头部但不出队列
                if(Thread.currentThread()==head){
                    //如果是队列头部则再次抢锁
                    if(!tryLock()){
                        //若失败,则挂起当前线程
                        LockSupport.park();
                    }else {
                        //若成功,将线程出队列并退出循环
                        waiters.poll();
                        break;
                    }
                }else {
                    //若不是头部,则挂起
                    LockSupport.park();
                }
            }
        }
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock() {
        return owner.compareAndSet(null,Thread.currentThread());
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    @Override
    public void unlock() {
        if(owner.compareAndSet(Thread.currentThread(),null)){
            Thread head = waiters.peek();
            LockSupport.unpark(head);
        }
    }

    @Override
    public Condition newCondition() {
        return null;
    }
}

测试结果:
在这里插入图片描述
这个锁就实现了。

发布了37 篇原创文章 · 获赞 10 · 访问量 685

猜你喜欢

转载自blog.csdn.net/weixin_41746577/article/details/103919322