多线程与高并发(2)

(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

猜你喜欢

转载自www.cnblogs.com/zdcsmart/p/12524785.html