线程安全之可见性(Volatile)和原子性(Atomic)

线程安全之可见性(Volatile)和原子性

  • 线程之可见性
    • 可见性原理分析
      • 线程间操作的定义
      • 同步的规则定义
      • Happens-before 先行发生原则
    • Final 修饰符
    • 线程可见性总结
  • 线程安全之原子性
    • 原子操作
    • 存在的问题及分析原因
      • 示例代码:
      • 存在的问题
    • 解决办法
      • 1) 借助sun.misc.Unsafe 类:
      • 2)使用 CAS(Compare and swap)

线程之可见性

可见性原理分析

可见性问题:让一个线程对共享变量的修改,能够及时的被其他线程看到。
JAVA 内存模型规定:
对volatile变量v的写入,与所有其他线程后续对v的读同步
要满足这些条件,所以volatile 关键字就有以下的功能:

  1. 禁止缓存;
    volatile 变量访问控制符会加个ACC_VOLATILE
    https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.2
  2. 对volatile 变量相关的指令不做重排序。

共享变量的定义
可以在线程之间共享的内存称为共享内存或堆内存。
所有实例字段,静态字段和数组元素都存在在堆内存中,这些字段和数组都是共享变量
冲突发生:如果至少有一个访问是写操作,那么对同一个变量的两次访问是冲突的。
这些能被多个线程访问的共享变量时内存模型规范的对象。

线程间操作的定义

1.线程间的操作是指:一个程序执行的操作可被其他线程感知或被其他线程直接影响。
2. Java 内存模型只描述线程间操作,不描述线程内操作,线程内操作按照线程内语义执行。线程间的操作有:

read 一般读操作,非volatile读
write 一般写操作,非volatile写
volatile read
volatile write
Lock.(锁monitor)、Unlock
线程的第一个和最后一个操作
外部操作
所有线程间的操作都存在可见性问题,JMM需要对其进行规范

同步的规则定义

对volatile变量v的写入,与所有其他线程后续对v的读同步。
对于监视器m的解锁与所有后续操作对于m的加锁同步。

Happens-before 先行发生原则

happens before 用于描述两个有冲突的动作之间的顺序,如果有一个action happends before 另一个action,则第一个操作被第二个操作可见,JVM 需要实现如下happens-before 规则:

某个线程中的每个动作都happens-before 该线程后面的动作。
某个管程上的unlock 动作和happens-before 同一个管程上后续的lock动作。
对某个volatile字段的写操作happens-before 每个后续对该volatile字段的读操作。
在某个线程对象上调用start() happens-before 被启动线程中的任意动作。
如果在线程t1 中成功执行了t2.join(),则t2 中的所有操作对t1可见。
如果某个动作a happens-before动作b,且b happens-before 动作c,则有 a happens-before c。

当程序包含两个没有被happens-before关系排序的冲突访问时,就称存在数据竞争,遵守了这个原则,也就意味着有些代码不能进行重排序,有些数据不能缓存!

Final 修饰符

final 在该对象的构造函数中设置对象的字段,当线程看到该对象时,将始终看到该对象的final 字段的正确构造版本。伪代码示例: f = new FinalDemo(); 读取到的f.x 一定是最新的值,x 为final 修饰的字段。
如果在构造函数中设置字段后发生读取,则会看到该final 字段分配的值,否则它将看到默认值:伪代码示例:pulbic finalDemo(){x=1;y=x} y会等于1;

Word Tearing 字节处理
有些处理器(尤其是早起的Alphas处理器)没有提供些单个字节的功能,在这样的处理器上更新byte数组,若只是简单的读取整个内容,更新对象的字节,然后将这个内容再写回内存,将是不合法的。
这个问题有时被分为“字分裂(word tearing)”更新单个字节有难度的处理器,就需要寻求其它方式来解决问题。因此在开发找那个尽量不要对byte[] 中的元素进行重新赋值,更不要在多线程程序这样做。

线程可见性总结

保证变量可见性的方式:

  1. final变量

  2. synchronized 关键字:
    synchronized 语义规范:
    1) 进入同步代码块前,先清空工作内存中的所有共享变量,从主内存中重新加载。
    2) 解锁前必须把修改的共享变量同步回主内存。
    注意:如果线程竞争两把不同的锁则不能保证
    如何保证线程安全:
    1) 锁机制保护共享资源,只有获得锁的线程才可操作共享资源。
    2) Synchronized 语义规范保证了修改共享资源后,会同步主内存,就做到了线程安全。

  3. 用volatile修饰
    1) 使用volatile变量时,必须重新从主内存加载,并且read,load是连续的。
    2) 修改volatile变量时,必须立马同步回主内存,并且store,write是连续的。

线程安全之原子性

原子操作

原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分(不可中断性)。
将整个操作视为一个整体,资源在该次操作中保持一致,这是原子性的核心特性。

存在的问题及分析原因

示例代码:

/**
* 实现累加的功能
*/
public class Counter {

    volatile int i = 0;

    public void add() {
        i++;
    }
}
/**
*  累加器的测试类
*/
public class CounterTest {

    public static void main(String[] args) throws InterruptedException {

        Counter counter = new Counter();

        for (int i = 0; i < 8; i++) {

            new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    counter.add();
                }
                System.out.println("done...");
            }).start();

        }

        Thread.sleep(6000L);

		//期望的结果是:i*j 为循环次数 = 80000 
        System.out.println(counter.i);

    }
}

输出的结果:

done...
done...
done...
done...
done...
done...
done...
done...
27661

存在的问题

输出结果最终是: 27661 并不是我们所期望的80000。再多运行几次呢,输出的结果也是每次都不同,但都不是我们想要的值。那么我们想到可能线程并发的问题,我们将累加器加上锁或者加上synchronized 关键字试试看:
1) 使用synchronized 关键字实现的Counter :

/**
* 加synchronized 的 Counter
*/
public class Counter {

    volatile int i = 0;

   	// 已加上同步关键字
    public synchronized void add() {
        i++;
    }
}

2) 使用 ReentrantLock 实现的Counter

/**
* 加锁的Counter 
*/
public class Counter {

    volatile int i = 0;
    Lock lock = new ReentrantLock();

   //已加上锁
    public void add() {
        lock.lock();

        i++;

        lock.unlock();
    }
}

再使用上面的CounterTest 分别调用不同写法的Counter 类,发现运行结果都是我们期望的80000:

done...
done...
done...
done...
done...
done...
done...
done...
80000

这就说明以上我们的猜测是正确的,但是为什么会出现不同的结果呢?接下来看以下分析:
首先我们还是将Counter 类还原到最初的状态(不加锁也不加synchronized):
然后进入到Counter.class 所在的目录打开终端执行(反编译查看字节码):

javap -v -p Counter.class

字节码显示:

  Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field i:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field i:I
        10: return

将以上的指令码对应内存分析,假如现在有t1 线程,按照从上到下的指令码执行,无论加多少次,结果是我们所期望的。
线程安全之可见性(Volatile)和原子性(Atomic)_第1张图片
以上是只有一个线程的情况,现在又加入了一个线程t2, 按照同样的指令码从上到下执行,t1,t2 执行完一次后 i 的结果是1而不是2,前面讲到原子操作,是一个不可中断的操作。 按照下图可以知道,对于t1线程来说,从上到下执行的指令码应为一个原子操作,不可让其它线程同时执行。由于前面提到可以使用锁机制来解决这一系列问题,但是现有另一种实现反方式!
线程安全之可见性(Volatile)和原子性(Atomic)_第2张图片

解决办法

1) 借助sun.misc.Unsafe 类:

public class CounterUnsafe {

    volatile int i = 0;

    private static Unsafe unsafe = null;

    private static long valueOffset;

    static {

        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);

            Field declaredField_i = CounterUnsafe.class.getDeclaredField("i");
            valueOffset = unsafe.objectFieldOffset(declaredField_i);

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

    /**
     * 自旋
     */
    public void add() {

        for (; ; ) {
            int current = unsafe.getIntVolatile(this, valueOffset);
            if (unsafe.compareAndSwapInt(this, valueOffset, current, current + 1)) {
                break;//如果修改不成功就自旋,修改成功就退出循环
            }
        }
    }

使用CounterTest 类来创建CounterUnsafe 对象作为counter 来调用add():


public class Demo1_CounterTest {

    public static void main(String[] args) throws InterruptedException {

	//创建CounterUnsafe  对象:
        CounterUnsafe counter = new CounterUnsafe();


        for (int i = 0; i < 8; i++) {

            new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    counter.add();
                }
                System.out.println("done...");
            }).start();

        }

        Thread.sleep(6000L);
        
		//最终输出的结果是我们期望的结果:80000
        System.out.println(counter.i);

    }
}
 if (unsafe.compareAndSwapInt(this, valueOffset, current, current + 1)) {...}

如果您觉得以上条件判断眼熟,那么您一定了解过:java.util.concurrent.atomic (并发原子类的包路径)
线程安全之可见性(Volatile)和原子性(Atomic)_第3张图片

2)使用 CAS(Compare and swap)

Compare and swap 比较和交换。属于硬件同步原语,处理器提供了基本内存操作的原子性保证。
CAS 操作需要输入两个值,一个旧值A(期望操作前的值)和一个新值B, 在操作期间先对旧值进行比较,若没有发生变化,才交换成新值,发生了变化则不交换。
JAVA 中的sun.misc.Unsafe 类,提供了compareAndSwapInt()compareAndSwapLong() 等几个方法实现CAS。
线程安全之可见性(Volatile)和原子性(Atomic)_第4张图片

CAS 原理
线程安全之可见性(Volatile)和原子性(Atomic)_第5张图片
同时有多个操作去修改同一个值时,先比较内存中期望的值是否为CAS中的第一个参数,如果是则用新值替换旧值,如果不是则不修改(自旋)。

CAS 操作的缺点

  1. 循环+cas ,自旋的实现让所有线程都处于高频运行,争抢CPU执行时间的状态。如果操作长时间不成功,会带来很大的CPU资源消耗。
  2. 仅针对单个变量的操作,不能用于多个变量来实现原子操作。
  3. ABA 问题。

ABA 问题

线程1,线程2同时执行CAS(0,1)操作,将i 的值由0 改为1
假设线程1操作成功,线程2操作失败
紧接着线程1执行了CAS(1,0),将i 的值改回0

ABA 问题的解决办法
在每次CAS操作中带上版本号,作为每次的操作的区分。AtomicStampedReference 中的源码,initialStamp为初始化的版本号:
在这里插入图片描述
此处使用 AtomicInteger 实现累加的具体示例:

累加器:

public class CounterAtomic {

    AtomicInteger i = new AtomicInteger(0);

    public void add() {
        i.incrementAndGet();
    }
}

测试类:

public class Demo1_CounterTest {

    public static void main(String[] args) throws InterruptedException {

        CounterAtomic counter = new CounterAtomic();
        for (int i = 0; i < 8; i++) {

            new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    counter.add();
                }
                System.out.println("done...");
            }).start();

        }

        Thread.sleep(6000L);
        System.out.println(counter.i);

    }
}

你可能感兴趣的:(JAVA,基础,线程可见性,线程原子性)