当前位置: 首页>后端>正文

java并发基础-线程安全、volatile和CAS

线程安全

线程安全是指当多个线程读写同一个共享资源,并且没有任何同步措施时时,会导致出现咱数据或其他不可预见结果的问题

主要有以下三个问题

  1. 原子性
    正常情况下两个线程对同一个公共变量赋值,其结果应该是两者中的其中一个值,因为理想的赋值情况是原子化的,不会被其他线程打扰,但事实上在java虚拟机中这个值很有可能不属于两个值中的任意一值,而是一个不可能存在的数值。
    比如32位系统下long类型的读写。
  2. 可见性
    可见性是当一个线程修改了一个共享变量的值,另一个线程是否能立即知道。
    在并行程序中,其实是不一定的
  3. 有序性
    在java代码编译时,在保证串行语义一致的前提下,其实是有可能会进行指令重排,这样就可能打乱代码执行的顺序,在串行程序中没有问题,但是在并行程序中就会出问题。
    比如线程a的函数中需要先给共享变量x、y赋值,最后给一个共享变量flag赋值为true,另一个线程b在判断该共享flag变量为true后,将执行x+y操作。但是由于指令重排,a线程给共享变量flag赋值为true的操作排在了第一位,,还没给x、y赋值的时候,b线程执行了x+y,其结果就和预期不符了。

以上是相对微观层面的影响线程安全的因素,即使以上三点都保证没有问题了,还有相对宏观层面的原子性需要保证
但两个线程同时操作一个共享变量时,其“读->写”的过程应当保持原子性,且与其他线程互斥,这就涉及到了锁操作(当然也有无锁保证原子化的CAS操作)
比如A、B两个线程并发操作一个计数器i,作用是记录函数执行次数,i的初始值为0,理想状态是A、B线程执行完后,i的值应为2。但有可能当A线程读取i的值为0,计数器加1,将要为i赋值为1前,B线程也读取了i的值为0,也加1,并为i赋值,此时A、B都为计数器赋值为1。实际效果与预期不符

volatile

多线程下处理共享变量时java线程模型如下


java并发基础-线程安全、volatile和CAS,第1张
图片.png

由于每个线程都有自己的私有内存,当一个线程处理共享变量时,他会从主内存将变量复制到自己的私有内存,处理完再把变量更新到主内存。
也就是说如果一个线程修改了变量,并且变量值也同步到主内存中了,但是另一个线程读取的一直是自己私有内存中的变量值,它意识不到这个变量值已经被其他的线程修改,这样共享变量的可见性就无法保证。
当然加锁可以解决这个问题,但除此之外还可以使用volatile关键字定义变量
当写入volatile变量值时,会把内容从私有内存同步至主内存;当读取volatile变量值时,会清空私有内存变量值,再从主内存获取最新值。
这就解决了可见性,和内存层面的原子性,使用volatile定义的变量也不会进行指令重排

CAS

如果不加锁,只用volatile依然无法保证多线程下共享变量"读->写"时的原子性。
使用锁的话,没有用到锁的线程会被挂起,线程挂起、再继续执行,其中的上下文切换和重新调度的开销还是相对比较高的(虽然jvm有优化,获取不到锁会先自旋一下下再挂起---自旋锁)。于是就有了CAS的操作(Compare and Swap),从字面意思就能看出,就是“比较和交换”,它是JDK提供的非阻塞性原子操作,它通过硬件保证了“比较和交换”的原子性。
ps:相较于纯纯使用synchronized加锁保证线程安全的Hashtable<K,V>ConcurrentHashMap这种使用ynchronized关键字+CAS操作的效率更高些
CAS函数通常有三个值,需要更新的变量,预期值,新值
CAS的操作类似于乐观所,只有当变量的值等于预期值时,才会将变量更新为新值。这个操作直觉上和把在代码中对变量值和预期值进行比较,成功了再更新变量一样,但实际上是不同的,CAS是在硬件层做的,我们还是要相信硬件层的操作是原子化的。
看一个CAS的方法,是个native方法

java并发基础-线程安全、volatile和CAS,第2张
图片.png

CAS方法在Unsafe类中,该类提供了硬件级的原子操作,但是该类所在的rt.jar包是用Bootstrap类加载器加载的,main函数所在类是用AppClassLoader加载的,在Unsafe.getUnsafe()方法中对类加载器做了判断,不让我们使用该类。
要用的话只能用反射来实现了
package com.jenson;

import sun.misc.Unsafe;
import java.lang.reflect.Field;

public class UnsafeTest {

    // 获取 Unsafe 实例
//    static final Unsafe unsafe = Unsafe.getUnsafe();
    static Unsafe unsafe;
    // 记录变量state在该类UnsafeTest中的偏移值
    static long stateOffset;

    private volatile long state = 0;

    static {
//        try {
//            // 获取变量state在该类UnsafeTest中的偏移值
//            stateOffset = unsafe.objectFieldOffset(UnsafeTest.class.getDeclaredField("state"));
//        } catch (NoSuchFieldException e) {
//            e.printStackTrace();
//        }

        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(Boolean.TRUE);

            unsafe = (Unsafe) field.get(null);

            stateOffset = unsafe.objectFieldOffset(UnsafeTest.class.getDeclaredField("state"));

        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        UnsafeTest test = new UnsafeTest();

        Boolean success = unsafe.compareAndSwapInt(test, stateOffset, 0, 1);

        System.out.println(success);
    }
}

atomic包

java.util.concurrent.atomic包中的原子变量操作就是以CAS方式实现的
AtomicLong举例,incrementAndGet方法,以原子的方式加一

java并发基础-线程安全、volatile和CAS,第3张
图片.png

java并发基础-线程安全、volatile和CAS,第4张
图片.png

可以看到,他就是以循环的方式调用compareAndSwapLong直至成功

《实战java高并发程序设计》(第二版)
《java并发编程之美》


https://www.xamrdz.com/backend/35n1938262.html

相关文章: