2. 线程安全性

本章内容:

  原子性:AtomicXXX、CAS原理、Unsafe、AtomicLong&LongAddr、AtomicReference&AtomicReferenceFieldUpdater、AtomicStampReference

      锁:synchronized(修饰代码块、方法、静态方法、类)、Lock

  可见性:synchronized、volatile(读、写、使用)

  有序性:happens-before原则

 

  【线程安全性】当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程如何进行交替执行,并且在代码运行中不需要额外的同步或协同,这个类都行为正常,那么称这个类是线程安全的。

  

【可见性】一个线程对主内存的修改可被其他线程看到。

【有序性】一个线程观察其他线程的指令执行顺序,由于指令重排序的存在,该观察结果一般无序。


一、原子性——java.util.concurrent.atomic包/CAS

  【原子性】提供了互斥访问,同一时刻只有一个线程对它操作。

1.Atomic包

  ①原子更新基本数字类型:AtomicInteger、AtomicLong、AtomicBoolean;

  ②原子更新数组:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray;

  ③原子更新引用:AtomicReference、AtomicMarkableReference、AtomicStampedReference;

  ④原子更新属性:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater;

 1 public static AtomicInteger count = new AtomicInteger(1);
 2 
 3 
 4 public static Student[] value = new Student[]{new Student(1),new Student2,…};
 5 public static AtomicReferenceArray arr = new AtomicReferenceArray(value);
 6 
 7 private static AtomicIntegerFieldUpdater updater = AtomicIntegerFieldUpdater.newUpdater(Student.class, "age");
 8 public volatile int age= 20;
 9 //student1是Student一个实例
10 updater.compareAndSet(studen1, 100, 120)

 

2. 线程安全性_第1张图片

 

2. CAS/Unsafe

  【Unsafe】Java无法直接访问底层操作系统,而是通过本地方法(native)来访问的。JVM开启一个后门,JDK有一个类Unsafe,它提供了硬件级别的原子操作。这个类中的方法都是public的,但是只有授信的代码才能获得该类的实例。

 

   【CAS】Compare and Swap:比较并交换。设计并发算法时常用的一门技术,JUC完全建立在CAS上,当前处理器都支持CAS。

  CAS方法有三个操作数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值与内存值相同时,将内存值修改为B并返回true。

  举例:

  AtomicInteger.getAndIncrement():

2. 线程安全性_第2张图片

  Unsafe.getAndAddInt():var1是当前AtomicInteger对象,var2是内存值V,var5是旧的预期值A,var5+var4是要修改的值,此处var4=1.只有当var2=var5时,才会赋予新值。

2. 线程安全性_第3张图片

 

   Unsafa.compareAndSwapInt():

 

   【ABA问题】如果一个变量V初次读取的时候是A值,并且在准备复制的时候检查它任然是A值,但是处理过程中被其他线程修改过A-B-A,CAS操作仍然认为他没有修改过=》ABA问题。

  解决方案:带有标记的原子引用类AtomicStampedReference,通过控制变量的版本来保证原子性,但是比较鸡肋,如果使用传统的互斥同步可能会使原子类更加高效。

 

3.AtomicLong && LongAddr

  Java8推荐LongAddr替代AtomicInteger。AtomicInteger在高并发场景下compareAndSwapInt会不断试错,有性能问题。 

  LongAdder在AtomicLong的基础上将单点的更新压力分散到各个节点,在低并发的时候通过对base的直接更新可以很好的保障和AtomicLong的性能基本保持一致,而在高并发的时候通过分散提高了性能。将待处理的数据,通过hash计算分散到Cell数组中分别处理,最后在汇总计算。注意casBase   ==>   !casBase(b = base, b + x);表示CAS更新操作;如果一个线程去CAS失败,那么表示正在有一个线程正在CAS操作,表示竞争激烈;

2. 线程安全性_第4张图片

 

   LongAddr在高并发的时候,Cell数组的汇总计算会出错。在高并发获取全局唯一ID时,使用AtomicLong而不是LongAddr。

 

4. AtomicBoolean

  使用场景——使某段代码只执行一次。首先我们要知道compareAndSet的作用,判断对象当时内部值是否为第一个参数,如果是则更新为第二个参数,且返回ture,否则返回false。那么默认初始化为false,则一个线程把他变为ture,compareAndSet返回ture,进入方法体执行逻辑,那么其他的任何线程进入该方法执行compareAndSet时第一个参数为false,而对象的内部值已经被修改为true,则永远过不了if。

private static AtomicBoolean isHappened = new AtomicBoolean(false);

private static void test() {
        if (isHappened.compareAndSet(false, true)) {//只执行一次
            log.info("execute");
        }
}

 

5. 锁

 【synchronized关键字】synchronized修饰的变量是不能被继承的,父类是synchronized,子类必须显式写出来,否则不是同步方法。

   【作用范围】修饰代码块、修饰静态方法、修饰非静态方法、修饰一个类。

   【原理】基于Monitor实现的。synchronized获取对象锁保证在执行共享数据的线程是互斥的,可以使用wait、notify、notifyAll进行线程协同工作。Class和Object都关联了一个Monitor。提供了两个高效的字节码指令monitorentermonitorexit实现同步代码块。同步加锁的是对象而不是代码。当一个线程访问Object的一个synchronized(this)同步代码块时,其他线程对Objecet中其他synchronized(this)同步代码块的访问将会被阻塞,只能访问非synchronized代码块。

 【锁-Lock】锁标记存放在对象头中的Mark Word中。

 【synchronized和Lock的区别

    ①Lock是一个接口,synchronized是一个关键字,由Java内置语言实现的。

    ②synchronized发生异常时,会自动释放线程占有的锁(独占锁,也是悲观锁),因此不会有死锁现象发生。Lock发生异常时,必须通过unLock()去释放锁(乐观锁),否则会产生死锁现象。通常将unLock放在finally中去释放锁。

    ③Lock会让等待的锁中断,而synchronized不会,因此会产生阻塞现象。

    ④通过Lock可以知道有么有成功获取锁,而synchronized不行。

    ⑤Lock可提高多线程进行读操作的效率。

 

二、可见性

  【可见性】一个线程对主存的修改可被其他线程看到、

1.导致共享变量不可见的原因

  ①线程交叉执行;

  ②重排序结合线程交叉执行;

  ③共享变量更新后没有及时地在工作内存和主内存之间更新;

2.可见性-synchronized

  JMM关于synchronized的规定:

  ①线程解锁前,必须将共享变量的最新值刷回主存;(解锁-刷)

  ②线程加锁时,将清空工作内存中共享变量值,从而使用共享变量时需要从主内存重新读取最新的值。(加锁-取)

3.可见性-volatile

  通过加入内存屏障禁止重排序优化来实现。

  ①对volatile变量写操作时,会在写操作后加入一条store屏障指令,将本地内存中的共享变量刷回主存中。

  ②对volatile变量读操作时,会在读操作前加入一条load屏障指令,从主存中读取共享变量。

2. 线程安全性_第5张图片

 

   volatile不能保证原子性:

/*
1.count       线程1,2读取最新值 count=1
2.count++     线程1,2同时增加1  count=2
3.count       线程1,2将count写回主存,少加一次1    count=1 正确结果应该是2;根本原因:count++不是一个原子性操作
可以通过Atomic包实现原子性操作
*/ volatile int count = 1; count++;

 

  ③应用场景:适合用于修饰状态标记量

   使用条件:对变量的写操作不依赖于变量的当前值,或者确保只有单个线程更新变量值;该变量没有包含在具有其他变量的不变式中。

  ④volatile与synchronized区别

  volatile synchronized
作用范围      变量   变量、方法、类
保证可见性
保证原子性 ×
是否会造成阻塞 ×
是都可被编译器优化 ×

 

 三、有序性

  【有序性】程序执行的顺序按照代码的顺序执行

  【重排序】见上章

  【happens-before规则】见上章

你可能感兴趣的:(2. 线程安全性)