多线程2

volatile

作用

  • 保证线程可见性

    使用volatile,将会强制所有线程都去堆内存中读取变量的值

    -MESI(CPU的缓存一致性协议)

    详细可看

    https://www.cnblogs.com/z00377750/p/9180644.html

  • 禁止指令重排序(CPU级别的支持)

    • cpu级别增加读写屏障

图解说明:

image-20200814153856725

A B线程都用到一个变量,java默认是A线程中保留一份copy,这样如果B线程修改了该变量,则A线程未必知道,不知B线程何时去修改真正的变量的值,此时使用volatile关键字,会让所有线程都会读到变量的修改值

volatie和synchronized区别

  • volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住

  • volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别

  • volatile仅能实现变量的修改可见性不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性

    volatile并不能保证多个线程共同修改变量时所带来的不一致问题,也就是说volatile不能替代synchronized
    
  • volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞

  • volatile标记的变量不会被编译器优化,不会进行指令重排序;synchronized标记的变量可以被编译器优化

volatile其他

  1. volatile 引用类型(包括数组)只能保证引用本身的可见性,不能保证内部字段的可见性。

    public class T04_VolatileNotSync {
    	volatile int count = 0;
    	void m() {
    		for(int i=0; i<10000; i++) count++;
    	}
    	
    	public static void main(String[] args) {
    		T04_VolatileNotSync t = new T04_VolatileNotSync();
    		
    		List<Thread> threads = new ArrayList<Thread>();
    		
    		for(int i=0; i<10; i++) {
    			threads.add(new Thread(t::m, "thread-"+i));
    		}
    		
    		threads.forEach((o)->o.start());
    		
    		threads.forEach((o)->{
    			try {
    				o.join();
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    		});
    		
    		System.out.println(t.count);
    		
    		
    	}
    	
    }
    
    =============
     输出结果的原因是:volatile虽然能够保证count的可见性,但是不能够保障count++的原子性
    

    输出结果

    image-20200814160705502

CAS(compare and swap)

什么是CAS

CAS:Compare and Swap,即比较再交换。

jdk5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁。JDK 5之前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁。

CAS属于CPU原语级别的支持,正常程序不会被打断

CAS算法理解

CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

image-20210223114037788

CAS实现原理

CAS缺点

  • 循环时间太长;
  • 只能保证一个共享变量原子操作;
  • 会出现ABA问题;
循环时间长开销很大:

CAS 通常是配合无限循环一起使用的,我们可以看到 getAndAddInt 方法执行时,如果 CAS 失败,会一直进行尝试。如果 CAS 长时间一直不成功,可能会给 CPU 带来很大的开销。

只能保证一个变量的原子操作:

当对一个变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个变量操作时,CAS 目前无法直接保证操作的原子性。但是我们可以通过以下两种办法来解决:1)使用互斥锁来保证原子性;2)将多个变量封装成对象,通过 AtomicReference 来保证原子性。

什么是ABA问题?ABA问题怎么解决?

CAS 的使用流程通常如下:
1)首先从地址 V 读取值 A;
2)根据 A 计算目标值 B;
3)通过 CAS 以原子的方式将地址 V 中的值从 A 修改为 B。

但是在第1步中读取的值是A,并且在第3步修改成功了,我们就能说它的值在第1步和第3步之间没有被其他线程改变过了吗?

如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。Java并发包为了解决这个问题,提供了一个带有标记的类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。

AutomicXXX(CAS+ volatile)

相关概念

  • Java从JDK1.5开始提供了java.util.concurrent.atomic包,方便程序员在多线程环境下,无锁的进行原子操作。原子变量的底层使用了处理器提供的原子指令,但是不同的CPU架构可能提供的原子指令不一样,也有可能需要某种形式的内部锁,所以该方法不能绝对保证线程不被阻塞。

  • 原子类其内部实现不是简单的使用synchronized,而是一个更为高效的方式CAS (compare and swap) + volatile和native方法(同步的工作更多的交给了硬件),从而避免了synchronized的高开销,执行效率大为提升

  • 虽然基于CAS的线程安全机制很好很高效,但要说的是,并非所有线程安全都可以用这样的方法来实现,这只适合一些粒度比较小,型如计数器这样的需求用起来才有效,否则也不会有锁的存在了

  • 在Atomic包里一共有12个类,四种原子更新方式,分别是原子更新基本类型,原子更新数组,原子更新引用和原子更新字段。Atomic包里的类基本都是使用Unsafe实现的包装类

    20210223113841

代码详解

主要以AtomicInteger为例

  • 有参构造函数:从构造函数函数可以看出,数值存放在成员变量value中;
  • 成员变量value声明为volatile类型,说明了多线程下的可见性,即任何一个线程的修改,在其它线程中都会被立刻看到
  • compareAndSet方法(value的值通过内部this和valueOffset传递)
  • 接收2个参数,一个是期望数据(expected),一个是新数据(new);如果atomic里面的数据和期望数据一致,则将新数据设定给atomic的数据,返回true,表明成功;否则就不设定,并返回false。
posted @ 2021-06-08 14:55  知白守黑,和光同尘  阅读(78)  评论(0编辑  收藏  举报