• 多线程与高并发(2)Volatile+CAS


    (1)Volatile

    volatile的特性

    volatile变量具有下列特性:

    1. 可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
    2. 阻止编译时和运行时的指令重排。
    3. 原子性。这里所说的原子性是对任意单个volatile变量的读/写,但是类似于volatile++这种复合操作不具有原子性。
    package day02;
    
    import java.util.concurrent.TimeUnit;
    
    /**
     * @author: zdc
     * @date: 2020-03-19
     */
    public class _1VolatileTest {
       /* volatile*/ boolean  flag = true;
        public void m(){
            System.out.println("m start");
            while (flag){
    
            }
            System.out.println("m end");
        }
    
        public static void main(String[] args) {
            _1VolatileTest v = new _1VolatileTest();
            new Thread(v::m,"t").start();
    
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            v.flag = false;
        }
    }

    什么是可见性?

    可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。

    volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”,我们可以简单的理解为把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步。如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。下面的例子中两个类的执行效果是相同的。

    public class VolatileFeatureExample {
        volatile long v1 = 0L;
    
        public void set(long l) {
            v1 = l;
        }
    
        public void getAndIncrement() {
            v1++;
        }
    
        public long get() {
            return v1;
        }
    }
    ----------------------------------------------
    public class VolatileFeatureExample {
        long v1 = 0L;
    
        public synchronized void set(long l) {
            v1 = l;
        }
    
        public void getAndIncrement() {
            long temp = get();
            temp += 1L;
            set(temp);
        }
    
        public synchronized long get() {
            return v1;
        }
    }

    volatile是如何实现可见性的呢?

    Java的内存模型:

    指令在CPU中执行,CPU运行速度较快,因此为减少从内存中频繁读写数据的开销,在cpu与内存的操作之间,有个高速缓存的的区域

    获取数据流程:

    • 从缓存中获取Data
    • 缓存中存在,则直接返回
    • 缓存中不存在
      • 从内存中获取Data数据
      • 将Data数据写入缓存
      • 返回Data数据

    上面的流程中,第一步会导致一致性问题,分析如下

    若内存中Data已更新,但缓存中数据未更新,此时返回缓存中Data,返回的是旧数据。

    解决方案:

    • 总线上加LOCK#锁
      • 因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存
    • 缓存一致性协议
      • 在内存中数据发生变更后,同时使所有缓存中的数据失效,在访问缓存中数据时,优先判断是否已经失效,若失效则从内存中获取最新的数据回写到缓存中,并返回

    volatile

    线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存保存该线程读写共享变量的副本。因此也存在上面的一致性问题,即如何保证线程对共享变量的修改后,其他的线程能访问到最新的共享变量。

    指令重排序

    Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性

    举例说明

    int i;
    boolean ans;
    
    i = 10;
    ans = true;
    

    上面的代码中,ians的赋值先后顺序由于指令重排,可能会出现ans=true时,i依然为0的情况。

     Volatile关键字

    用法

    • 在变量前面加上volatile即可

    作用

    • 确保共享变量的修改,对其他线程都是立即可见的
    • 禁止指令重排(即当访问or修改volatile修饰的共享变量时,确保前面的代码都执行完了)

    原理和实现机制

    • 修改volatile声明的共享变量,会强制要求修改后的值写入内存,并失效其他线程的本地内存中的副本
    • 汇编之后,加入volatile关键字时,会多出一个lock前缀指令
    • 它确保指令重排序时不会把其后面的指令排到lock指令之前,也不会把前面的指令排到lock指令之后

    例子::Java并发-懒汉式单例设计模式加volatile的原因

    class SingletonClass{
     2     private static  SingletonClass instance = null;
     3  
     4     private SingletonClass() {}
     5  
     6     public static  SingletonClass getInstance() {
     7         if(instance==null) {
     8             synchronized ( SingletonClass.class) {
     9                 if(instance==null)
    10                     instance = new  SingletonClass();//语句1
    11             }
    12         }
    13         return instance;
    14     }
    15 }

    上面的代码在多线程下调用可能会报错,具体报错原因:

    在语句1中并不是一个原子操作,在JVM中其实是3个操作:
    1.给instance分配空间、
    2.调用 Singleton 的构造函数来初始化、
    3.将instance对象指向分配的内存空间(instance指向分配的内存空间后就不为null了);
    在JVM中的及时编译存在指令重排序的优化,也就是说不能保证1,2,3执行的顺序,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是 1-3-2,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
      通过添加volatile就可以解决这种报错,因为volatile可以保证1、2、3的执行顺序,没执行玩1、2就肯定不会执行3,也就是没有执行完1、2instance一直为空

     

    锁优化:

      锁细化:不应该把锁加在整个方法上。

      锁粗化:在征用特别频繁的地方。

    以对象做锁时,为使它不发生改变,应该加final。

     

    (2)CAS --无锁优化 或称自旋。

    package day02;
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.atomic.AtomicInteger;
    
    /**
     * @author: zdc
     * @date: 2020-03-19
     */
    public class _2ActomicInteger {
        //int count=0;
        AtomicInteger count = new AtomicInteger(0);
        void m(){
            for (int i = 0; i < 10000; i++) {
             count.incrementAndGet();
             //   count++;
            }
        }
    
        public static void main(String[] args) {
            _2ActomicInteger test = new _2ActomicInteger();
            List<Thread> threads = new ArrayList<Thread>();
            for (int i = 0; i < 10; i++) {
                threads.add(new Thread(test::m,"thread_"+i));
            }
    
            threads.forEach((t)->t.start());
            //让主线程最后运行 得到结果
            threads.forEach((t)->{
                try {
                    t.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            System.out.println(test.count);
        }
    }

    CAS算法理解 https://www.jianshu.com/p/ab2c8fce878b

    对CAS的理解,CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

    CAS比较与交换的伪代码可以表示为:

    do{

    备份旧数据;

    基于旧数据构造新数据;

    }while(!CAS( 内存地址,备份的旧数据,新数据 ))

     因为t1和t2线程都同时去访问同一变量56,所以他们会把主内存的值完全拷贝一份到自己的工作内存空间,所以t1和t2线程的预期值都为56。

    假设t1在与t2线程竞争中线程t1能去更新变量的值,而其他线程都失败。(失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次发起尝试)。t1线程去更新变量值改为57,然后写到内存中。此时对于t2来说,内存值变为了57,与预期值56不一致,就操作失败了(想改的值不再是原来的值)。

    CAS(比较并交换)是CPU指令级的操作,只有一步原子操作,所以非常快。而且CAS避免了请求操作系统来裁定锁的问题,不用麻烦操作系统,直接在CPU内部就搞定了.
    CAS操作是CPU指令级别上的支持,中间不会被打断。
     
    ABA问题:
    package day02;
    
    import java.sql.Time;
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.atomic.AtomicInteger;
    
    /**
     * @author: zdc
     * @date: 2020-03-19
     */
    public class _3ABATest {
        private static AtomicInteger count = new AtomicInteger(10);
    
        public static void main(String[] args) {
          //10-》11-》10
    new Thread(()->{ System.out.println(Thread.currentThread().getName()+"预期值是10?"+count.compareAndSet(10,11)); System.out.println(Thread.currentThread().getName()+"预期值是11?"+count.compareAndSet(11,10)); },"A").start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(()->{ System.out.println(Thread.currentThread().getName()+"预期值是10?"+count.compareAndSet(10,12)); },"B").start(); } }
    AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(10,1); 可以添加版本号   解决ABA

  • 相关阅读:
    JDK1.8源码之HashMap(一)——实现原理、查找及遍历
    JDK1.8源码之ArrayList
    03、Swagger2和Springmvc整合详细记录(爬坑记录)
    02、Java的lambda表达式和JavaScript的箭头函数
    Java-IO流之输入输出流基础示例
    JDBC API 事务的实践
    JDBC API 可滚动可编辑的结果集
    Java虚拟机----垃圾回收与内存分配
    Java数据库连接与查询
    Java虚拟机-对象的创建和访问
  • 原文地址:https://www.cnblogs.com/zdcsmart/p/12524785.html
Copyright © 2020-2023  润新知