• Java-JUC(三):原子性变量与CAS算法


    原子性

    并发程序正确地执行,必须要保证原子性可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

    原子性:一个操作或多个操作要么全部执行完成且执行过程不被中断,要么就不执行。

    可见性:当多个线程同时访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

    有序性:程序执行的顺序按照代码的先后顺序执行。

    对于单线程,在执行代码时jvm会进行指令重排序,处理器为了提高效率,可以对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证保存最终执行结果和代码顺序执行的结果是一致的。

    看下边的一个例子:

    package com.dx.juc.test;
    
    public class MyThread implements Runnable {
        private int serialNumber = 0;
    
        public void run() {
            System.out.println(Thread.currentThread().getName() + ":" + getSerialNumber());
        }
    
        public int getSerialNumber() {
            return serialNumber++;
        }
    }

    调用:

    package com.dx.juc.test;
    
    public class Main {
        public static void main(String[] args) {
            MyThread thread=new MyThread();
            for(int i=0;i<10;i++){
                new Thread(thread).start();
            }
        }
    }

    输出结果(有时会抛出下边异常结果,但不是每次都出现异常结果):

    Thread-1:0
    Thread-3:2
    Thread-2:1
    Thread-0:0
    Thread-4:3
    Thread-5:4
    Thread-7:5
    Thread-6:6
    Thread-9:7
    Thread-8:8

    上边这个操作错误的原因:

    1)初始值serialNumer=0,thread-0把该值复制到自己的工作空间(线程私有的,该工作空间也可以叫做缓存),之后进行了三个操作:

    操作一:int temp=serialNumber;// 从主存中将serialNumber复制到自己的工作空间

    操作二:serialNumber=serialNumber+1;// 在自己的工作空间内进行运算

    操作三:将serialNumber刷新到主存中

    2)假设thread-0在操作三还未处理之前,thread-1从主存中复制serialNumber=0到自己的工作空间,然后thread-0触发操作三,而此时thread-1并不知道主存中的serialNumber已经被修改,它依然使用工作空间中的serialNum=0,也进行与thread-0一样的散步操作。

    当thread-1触发操作三时(此时thread-0已经把主存中的serialNubmer修改为1)并不知道serialNumber是否被其他线程操作过,它就把在自己工作区修改的结果刷新到主存中,此时thread-1中的serialNum=1,之后的操作就是thread-1的值覆盖了thread-0的值,实际上他们值是一样的。

    3)即时把int serialNumber添加上volatile修饰也不能避免该问题,从这里可以看出volatile是不具有原子性的。

    在java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断,要么执行,要么不执行。

    X=10;   // 原子性(简单的读取、将数字赋值给变量)
    Y = x;  // 变量之间的相互赋值,不是原子操作
    X++;    // 对变量进行计算操作,此时讲过三次操作int temp=X;X=X+1;X=temp,经过获取、修改、赋值三个操作。
    X = x+1; 

    语句2实际包括两个操作,它先要去读取x的值,再将y值写入,两个操作分开是原子性的,合在一起就不是原子性的。

    语句3、4:x++  x=x+1包括3个操作:读取x的值,x+1,将x写入,所以他们也不是原子性的。

    只有语句1具有原子性。

    注:可以通过 synchronized和Lock实现原子性。因为synchronized和Lock能够保证任一时刻只有一个线程访问该代码块。

    使用Atomic解决原子性问题

    在jdk1.5以后java.util.concurrent.atomic包下,提供了

    大量的原子变量,它们内部使用CAS算法。

    针对上边的代码修改为如下:

    package com.dx.juc.test;
    
    import java.util.concurrent.atomic.AtomicInteger;
    
    public class MyThread implements Runnable {
        private AtomicInteger serialNumber = new AtomicInteger();
    
        public void run() {
            System.out.println(Thread.currentThread().getName() + ":" + getSerialNumber());
        }
    
        public int getSerialNumber() {
            return serialNumber.getAndIncrement();
        }
    }

    将int serialNumber修改AtomicInteger serialNumber就具有原子性,也不在出现异常结果。

    CAS算法

    CAS(Compare-And-Swap)是一种硬件对并发的支持,针对多处理器操作而设计的,处理器中的一种特殊指令,用于管理对共享数据的并发访问。

    CAS是一种无锁的非阻塞算法实现,是硬件对于并发操作的支持,保证了数据变量的原子性。

    Cas包含了3个操作数:

    1. 内存值 V
    2. 预估值 A
    3. 更新值 B

    当且仅当 V == A 时, V = B; 否则,不会执行任何操作。

    简单的来说,CAS有3个操作数,要读写的内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则返回V(不做任何操作,然后重新获取主存V值,重新操作。)。这是一种乐观锁的思路,它相信在它修改之前,没有其它线程去修改它。

    CAS算法模拟:

    模拟代码:

    package com.dx.juc.test;
    
    public class TestCompareAndSwap {
        public static void main(String[] args) {
            final CompareAndSwap cas = new CompareAndSwap();
    
            for (int i = 0; i < 10; i++) {
                new Thread(new Runnable() {
                    public void run() {
                        int expectedValue = cas.getValue();
                        boolean b = cas.compareAndSet(expectedValue, (int) new java.util.Random.nextInt(1000));
                        System.err.println(b);
                    }
                }).start();
            }
    
        }
    }
    
    /**
     * 默认CAS
     **/
    class CompareAndSwap {
        // 内存值
        private volatile int value = 0;
    
        // 返回内存值
        public synchronized int getValue() {
            return value;
        }
    
        /**
         * 如果预估值与原来的值一直,则修改内存为新的值,否则,不做处理。 无论是否修改,都返回原来的内存值。
         **/
        public synchronized int compareAndSwap(int expectedValue, int newValue) {
            int oldValue = value;
    System.out.println("old:" + oldValue + ",expectedValue:" + expectedValue + ",newValue:" + newValue);
    if (expectedValue == oldValue) { value = newValue; } return oldValue; } // 如果更新成功,舊的內內存值和預估值相等。 public synchronized boolean compareAndSet(int expectedValue, int newValue) { return expectedValue == compareAndSwap(expectedValue, newValue); } }

    输出结果:

    old:0,expectedValue:0,newValue:224
    trueold:224,expectedValue:224,newValue:303
    old:303,expectedValue:0,newValue:690
    old:303,expectedValue:0,newValue:500

    true
    false
    false
    old:303,expectedValue:303,newValue:639
    true
    old:639,expectedValue:0,newValue:525
    false
    old:639,expectedValue:303,newValue:978
    false
    old:639,expectedValue:639,newValue:734
    trueold:734,expectedValue:734,newValue:803

    old:803,expectedValue:734,newValue:455
    true
    false
  • 相关阅读:
    redis 配置文件
    mysql的join
    mysql在DOS下的操作
    Echart显示在顶端显示总数
    汇编中,BP,SP有何区别?分别怎么使用?
    汇编函数调用中bp和sp是指什么?
    汇编语言中,SP,BP ,SI,DI作用?
    我对读计算机软件专业硕士的几点看法
    磨刀不误砍柴工
    《自己动手写操作系统》读书笔记——初识保护模式
  • 原文地址:https://www.cnblogs.com/yy3b2007com/p/8908509.html
Copyright © 2020-2023  润新知