java高级-高性能-多线程并发编程--1.2.2 线程安全之原子操作

一串代码引出的问题

public class LockDemo{
   volatile  int i= 0;
   public void add(){
    i++;//实际起码三次操作 ?
   } 


  public static void  main(String[] args) throws InterruptedException{
     
     LockDemo Id = new LockDemo();
        for(int i = 0;i < 2 ;i++){
        new  Thread( ()->{
              for(int  j = 0;j<10000 ; j++ ){
                  Id.add();
              }
          }).start();
        }
Thread.sleep(2000L); 
System.out.println(Id.i);

   }
}

理论上应该是20000
实际结果 :14500
第二次实际结果 :15400(不可预测性)

i++;//实际起码三次操作 ? 字节码角度

阅读这个类的字节码文件你会发现这些步骤

  • 读取 i
  • 计算 i++
  • 赋值 i
    这时 两个线程去走读取这个步骤(没事 ,然后自己计算) ,
    写入的时候 如果同时写入 就会发生问题

在这里插入图片描述

在这里插入图片描述

概念了解

  • 临界区: 部分关键代码的多线程并发执行 ,会对执行结果产生影响 .如上面的 i++;

  • 竞态条件 :可能发生在临界区域内的特殊条件. 执行i++ 时产生了 竞泰条件(类似环境)

  • .共享资源
    1.如果一段代码是安全的,这它不包含 竞态条件. 只有多个线程更新共享资源时 才会发生竞态条件.
    2.栈封闭时,不会在线程之间共享的变量 .都是线程安全的. 你的流程没有用到共享变量 ,而是私有的.
    3.局部对象引用本身不共享 ,但是引用的对象存储在共享堆中. 如果方法内创建对象 ,只是在方法中传递 ,并且 不对其他线程可用 ,呢么线程也是安全的.

  • 不可变的共享对象—不可变对象 —线程安全
    实例被创建 ,value变量就不能被修改 ,这就是不可变性.

  • 原子操作(数据一致性的要求)
    可以是一步 ,也可以是 多步 操作. 但是顺序不可以被打乱 ,也不可以被切割而只执行一部分.
    将整个操作视为一个整体,资源在操作中保持一致 ,这就叫原子性

资源在操作中保持一致就是 我操作的这个数据 是确定的 而不是 正在或已经被修改了的样子

比较和替换 =机制[CAS机制] --提供了原子性保证 来解决这个问题

我在数据提交 ,赋值的 时候 看看 数据有没有变化 ,没变化在赋值(交换) ,变化了 就不操作
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
调用 硬件原语的流程:
java–jvm—c----010100010硬件驱动—内存
想要做到动内存里的信息 ,不是一步到位的 而且 我们输入两个值 这叫CAS操作(重点:硬件层次支持 )

一个旧值 A 未计算 1
一个新值 B 计算后 2
如图 两个线程都拿到A=1 操作,
结果: 两个线程 只有一个可以执行成功

compareAndSwapInt(对象 ,对象的属性偏移量 , 当前值 ,目标值);
JAVA中的sun.misc.Unsafe类提供compareAndSwapInt()和compareAndSwapLong()等几个方法实现CAS

代码演示:
JAVA中的sun.misc.Unsafe类提供compareAndSwapInt()和compareAndSwapLong()等几个方法实现CAS

//两个线程对 i 变量进行选择操作  +机制[CAS机制] 保证原子性
import java.lang.reflect.Field;
 
public class LockCASDemo{
    volatile int i = 0;//因为加了volatile之后,会告诉我们的编译器,不要去优化它
    //不能直接用 如下 会报错  所以我们用反射来写
   // Unsafe unsafe =  Unsafe.getUnsafe();  //翻译:不安全的类  我们就是靠他来实现CAS的
   //  unsafe.compareAndSwapInt(); //cas底层API 不是java 写的
      private static Unsafe unsafe = null;
       //i字段的偏移量
        private static long valueOffset;
 


static {
        try {
          // 通过反射获取Unsafe对象  这是拿到theUnsafe属性 静态
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);  //拿到对象 但我们没办法直接set ,get操作内存 存的属性 而是通过 对象及其属性 的定位地址 来做--- 这里用"偏移量"来定位
 
            Field fieldi = LockCASDemo.class.getDeclaredField("i");
            // 通过Unsafe对象获得i的偏移量
            valueOffset = unsafe.objectFieldOffset(fieldi);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }
  
   public void add() {
      //i++;  这是原来的写法 本质是三次操作 下面分开写出来本质还是i++ 然后 就加入比较 替换的概念
    
    do{ 
     int current = i; //读取当前值
     int value = current+1 ;
    }while (!unsafe.compareAndSwapInt(this, valueOffset, current,value )  );
 
    
 //    if(current==i){
//   i = value; //赋值 ---c s-这一步 的时候检查我原来取的数据是否还是原来的那个样子
 //    } else {//修改失败怎么办?
  //失败了就不管了              
 //  }  
        
    }
 
  public static void  main(String[] args) throws InterruptedException{
     
     LockDemo Id = new LockDemo();
        for(int i = 0;i < 2 ;i++){
        new  Thread( ()->{
              for(int  j = 0;j<10000 ; j++ ){
                  Id.add();
              }
          }).start();
        }
Thread.sleep(2000L); 
System.out.println(Id.i);

   }
}
  1. 系统执行那么快,这样马上比较 拿值前 拿值后 有意义吗?
    答:多线程执行 --短暂 且 无序 是有意义的

  2. 如何理解偏移量的概念?
    在这里插入图片描述
    偏移量用来找到属性 然后就可以改了 .
    而它怎么得到就有学问了, java 通过 告知对象的地址 (类.class)和 属性名字 给JVM
    然后 内存 会根据这些信息得到 这个对象的属性的相对位置 即偏移量

  3. 执行失败了怎么办?
    答:执行失败了当然要补上啊 , 一次不行 再来一次 可以使用循环语句解决

  4. 这样的解决方案完美吗?还是会有新的问题产生.–消耗大, 多个就不行了 ,等

1.循环+CAS,自旋的实现让所有线程都处于高频运行,争抢CPU执行时间的状态。如果操作长时间不成功,会带来很大的CPU资源消耗。
2.仅针对单个变量的操作,不能用于多个变量来实现原子操作。
3. ABA问题。(无法体现出数据的变动) 。
为了解决这个问题 我们写了很复杂的代码 过于繁琐(java包帮我们打包了哦)

ABA 无锁编程的缺点 如图 是一种少见的情况 只知道结果不知道过程
在这里插入图片描述

优化一 解决代码繁琐的问题

AutomicInteger原子操作类实现原理

public class LockAutomicDemo{
    volatile AutomicInteger i = new AutomicInteger();
    public  void add() {
    i.incrementAndGet();
  }


/**源码:
 * AutomicInteger的incrementAndGet()方法源码
 *
 * Atomically increments by one the current value.
 *
 * @return the updated value
 */
public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

在这里插入图片描述

优化二 解决 ++ – 操作消耗大的问题

说明: 线程多 竞争大 消耗就大 ,只有一个能改 ,其他人失败了 就再来

在这里插入图片描述

原理 提供多个变量 供 线程修改(分而治之的思路)

在这里插入图片描述

优化三 AtomicStampedReference (包装了一个对象)通过加版本号解决ABA问题

// 存储在栈里面元素 -- 对象
public class Node {
    public final String value;
    public Node next;
 
    public Node(String value) {
        this.value = value;
    }
 
    @Override
    public String toString() {
        return "value=" + value;
    }
}


// 存在ABA问题的栈(后进先出)
public class Stack {
    // top cas无锁修改
    AtomicReference<Node> top = new AtomicReference<Node>();
 
    public void push(Node node) { // 入栈
        Node oldTop;
        do {
            oldTop = top.get();
            node.next = oldTop;
        } while (!top.compareAndSet(oldTop, node)); // CAS 替换栈顶
    }
 
    // 出栈 -- 取出栈顶 ,为了演示ABA效果, 增加一个CAS操作的延时
    public Node pop(int time) {
 
        Node newTop;
        Node oldTop;
        do {
            oldTop = top.get();
            if (oldTop == null) { // 如果没有值,就返回null
                return null;
            }
            newTop = oldTop.next;
            if (time != 0) { // 模拟延时
                LockSupport.parkNanos(1000 * 1000 * time); // 休眠指定的时间
            }
        } while (!top.compareAndSet(oldTop, newTop)); // 将下一个节点设置为top
 
        return oldTop; // 将旧的Top作为值返回
    }
}


// 使用AtomicStampedReference消除ABA问题的栈
public class ConcurrentStack {
    AtomicStampedReference<Node> top = new AtomicStampedReference<>(null, 0);
 
    public void push(Node node) { // 入栈
        Node oldTop;
        int v;
        do {
            v = top.getStamp();// 当前top的版本号
            oldTop = top.getReference();
            node.next = oldTop;
        } while (!top.compareAndSet(oldTop, node, v, v + 1)); // CAS 替换栈顶
    }
 
    // 出栈 -- 取出栈顶 ,为了演示ABA效果, 增加一个CAS操作的延时
    public Node pop(int time) {
        Node newTop;
        Node oldTop;
        int v;
        do {
            v = top.getStamp();
            oldTop = top.getReference();
            if (oldTop == null) { // 如果没有值,就返回null
                return null;
            }
            newTop = oldTop.next;
            if (time != 0) { // 模拟延时
                LockSupport.parkNanos(1000 * 1000 * time); // 休眠指定的时间
            }
        } while (!top.compareAndSet(oldTop, newTop, v, v + 1)); // 将下一个节点设置为top
 
        return oldTop; // 将旧的Top作为值返回
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值