【并发编程-基础】(三)线程安全性

一、线程安全性的定义

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

二、线程安全性指标

1.1、原子性

       提供了互斥访问,同一时刻只能有一个线程来对它进行操作。Java中保证同一时刻只有一个线程对某对它进行操作的,除了Atomic包之外,还有锁的机制。

1.2、可见性

       一个线程对主内存的修改可以及时的被其它线程观察到。

1.3、有序性

       一个线程观察其它线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序。

三、原子性总结

2.1、使用具有原子性的操作
package com.tangxz._4.example.atomic;

import com.tangxz.annoations.ThreadSafe;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @Info: 使用具有原子性的操作
 * @Author: tangxz
 * @Date: 2019/12/8  9:25
 */
@Slf4j
@ThreadSafe
public class AtomicExample1 {
    private static int threadTotal = 200;//同时最多200个请求同时执行
    private static int clientTotal = 5000;//总共5000个请求

    private static AtomicInteger count = new AtomicInteger(0);//获取到的数量

    public static void main(String[] args) throws InterruptedException {
        //使用线程池和信号量来模仿客户端
        ExecutorService executorService = Executors.newCachedThreadPool();//线程池
        final Semaphore semaphore = new Semaphore(threadTotal);//信号量
        //保证线程完全完整的执行
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int index = 0;index<clientTotal;index++){
            executorService.execute(()->{
                try{
                    semaphore.acquire();
                    add();
                    semaphore.release();
                }catch (Exception e){
                   log.error("exception",e);
                }
                //执行完就-1
                countDownLatch.countDown();
            });
        }
        //保证线程全部执行完成
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}",count.get());
    }
    private static void add(){
        //先增加再获取当前值,该方法具备原子性
        count.incrementAndGet();
        //先获取当前值,再增加
		//count.getAndAccumulate();
    }
}
/*方法输出:00:31:46.026 [main] INFO com.tangxz._4.example.atomic.AtomicExample1 - count:5000*/
2.2、Atomic
       2.2.1、AtomicIntegerincrementAndGet()方法具备原子性
  • 在测试中通过CountDownLatchcountDown()方法 ( 执行完一次就减一 ) 和await()方法 ( 保证线程全部都执行 ) 来确保准确度

  • Atomic包下的类的incrementAndGet()方法使用了一个unsafe的类的getAndAddInt方法实现count+1操作。

  • 里面会有一个do{}while()的条件,当当前值 ( 该对象的当前值var5 ) 与预期值 ( 传入方法的当前值var2 ) 相同时才会更新。compareAndSwapInt(CAS)的核心就是该对象的核心

public final int incrementAndGet() {
     return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    return var5;
}

       AtomicLongAtomicInteger是一样的效果,一样的使用。

       2.2.2、LongAdder 解析
  • JDK8新类,线程高的情况优先使用
  • 为什么有了AtomicLong还要新增一个LongAdder呢?
  • CAS底层实现是在一个死循环中不断地尝试修改目标值,直到修改成功。如果竞争不激烈的时候,修改成功率很高,否则失败率很高。在失败的时候,这些重复的原子性操作会耗费性能。
  • 对于普通类型的longdouble变量,JVM允许将64位的读操作或写操作拆成两个32位的操作。
  • LongAdderAtomicLong的基础上将单点的更新压力分散到各个节点,在低并发的时候通过对base的直接更新可以很好的保障和AtomicLong的性能基本保持一致,而在高并发的时候通过分散提高了性能。
  • 缺点是LongAdder在统计的时候如果有并发更新,可能导致统计的数据有误差。
       2.2.3、AtomicReferenceAtomicReferenceFieldUpdater 的使用
package com.tangxz._4.example.atomic;

import com.tangxz.annoations.ThreadSafe;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.atomic.AtomicReference;

/**
 * @Info: AtomicReference的使用
 * @Author: 唐小尊
 * @Date: 2019/12/8  9:25
 */
@Slf4j
@ThreadSafe
public class AtomicExample4 {
    private static AtomicReference<Integer> count = new AtomicReference<>(0);

    public static void main(String[] args) {
        //如果此时count等于0时,count变为2
        count.compareAndSet(0,2);
        //如果此时count等于0时,count变为1
        count.compareAndSet(0,1);
        count.compareAndSet(1,3);
        count.compareAndSet(2,4);
        count.compareAndSet(3,5);
        log.info("count:{}",count.get());
    }
}
/*方法输出:00:32:25.827 [main] INFO com.tangxz._4.example.atomic.AtomicExample4 - count:4*/
package com.tangxz._4.example.atomic;

import com.tangxz.annoations.ThreadSafe;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

/**
 * @Info: AtomicReferenceFieldUpdater的使用
 * @Author: 唐小尊
 * @Date: 2019/12/8  9:25
 */
@Slf4j
@ThreadSafe
public class AtomicExample5 {
    //用的不多
    private static AtomicIntegerFieldUpdater<AtomicExample5> updater = AtomicIntegerFieldUpdater.newUpdater(AtomicExample5.class,"count");

    @Getter
    public volatile int count = 100;

    private static AtomicExample5 example5 = new AtomicExample5();
    public static void main(String[] args) {
        if (updater.compareAndSet(example5,100,120)){
            log.info("update success,{}",example5.getCount());
        }
        if (updater.compareAndSet(example5,100,120)){
            log.info("update success,{}",example5.getCount());
        }else {
            log.info("update failed,{}",example5.getCount());
        }
    }
}
/*方法输出:00:32:38.594 [main] INFO com.tangxz._4.example.atomic.AtomicExample5 - update success,120
00:32:38.599 [main] INFO com.tangxz._4.example.atomic.AtomicExample5 - update failed,120*/
       2.2.4、AtomicStampReferenceCASABA 问题
  • CAS是上方调用的compareAndSwapInt方法的ABA问题
  • 每次更改变量都会改变该变量的版本号,用法跟其它的Atomic类相似。
  • ABA问题:CAS操作的时候,其他线程将变量的值A改成了B,但是随后又改成了A,本线程在CAS方法中使用期望值A与当前变量进行比较的时候,发现变量的值未发生改变,于是CAS就将变量的值进行了交换操作。但是实际上变量的值已经被其他的变量改变过,这与设计思想是不符合的。所以就有了AtomicStampReference
       2.2.5、AtomicBoolean 的使用
package com.tangxz._4.example.atomic;

import com.tangxz.annoations.ThreadSafe;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @Info: AtomicBoolean的原子性测试
 * @Author: 唐小尊
 * @Date: 2019/12/8  9:25
 */
@Slf4j
@ThreadSafe
public class AtomicExample6 {
    private static AtomicBoolean isHappened = new AtomicBoolean(false);
    private static int threadTotal = 200;
    private static int clientTotal = 5000;

    
    public static void main(String[] args) throws InterruptedException {
        ExecutorService exec = Executors.newCachedThreadPool();//线程池
        final Semaphore semaphore = new Semaphore(threadTotal);//信号量
        //保证线程完全完整的执行
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int index = 0; index < clientTotal; index++) {
            exec.execute(() -> {
                try {
                    semaphore.acquire();
                    test();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        log.info("isHappened:{}", isHappened);
    }

    private static void test() {
        if (isHappened.compareAndSet(false, true)) {
            log.info("execute");
        }
    }
}
/*方法输出:00:33:52.849 [pool-1-thread-1] INFO com.tangxz._4.example.atomic.AtomicExample6 - execute
00:33:52.909 [main] INFO com.tangxz._4.example.atomic.AtomicExample6 - isHappened:true*/
2.3、原子性-锁
       2.3.1、synchronized

       依赖JVM去实现锁,因此在这个关键字作用对象的作用范围内,都是同一时刻只能有一个线程对其进行操作的。

       synchronized是java中的一个关键字,是一种同步锁。它可以修饰的对象主要有四种:

  • 修饰代码块:大括号括起来的代码,作用于调用的对象

  • 修饰方法:整个方法,作用于调用的对象
    ———————————————————————–

  • 修饰静态方法:整个静态方法,作用于所有对象

  • 修饰类:括号括起来的部分,作用于所有对象

       2.3.2、Lock

       依赖特殊的CPU指令,代码实现,ReentrantLock

2.3.3、原子性对比
  • synchronized:不可中断锁,适合竞争不激烈,竞争激烈时性能下降特别快。(经常使用,面试经常被问)
  • Lock:可中断锁,多样化同步,竞争激烈时能维持常态。
  • Atomic:竞争激烈时能维持常态,比Lock性能好;只能同步一个值。(经常使用,面试经常被问)

四、可见性总结

4.1、导致共享变量在线程间不可见的原因
  1. 线程交叉执行
  2. 重排序结合线程交叉执行
  3. 共享变量更新后的值没有在工作内存与主存间及时更新(Java内存模型会了这个就理解了)
4.2、JMM关于synchronized的两条规定:
  1. 线程解锁前,必须把共享变量的最新值刷新到主内存
  2. 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意,加锁与解锁是同一把锁)
4.3、可见性 – volatile
  • 通过加入内存屏障和禁止重排序优化来实现
  • volatile变量写操作时,会在写操作后加入一条store屏障指令,将本地内存中的共享变量值刷新到主内存。
  • volatile变量读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量。
  • 通俗的说,这个变量在每次被线程访问时,都强迫从主内存中读取该变量的值,而当该变量发生变化的时候,又会强迫线程将最新的值刷新到主内存,这样的话,不同的时候,任何的线程都能看到a变量的最新值

在这里插入图片描述

4.4、使用volatile测试其原子性(不具备原子性)
package com.tangxz._4.example.count;

import com.tangxz.annoations.NotThreadSafe;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

/**
 * @Info: volatile原子性测试
 * @Author: 唐小尊
 * @Date: 2019/12/8  9:25
 */
@Slf4j
@NotThreadSafe
public class VolatileCountExample4 {
    private static int threadTotal = 200;//同时最多200个请求同时执行
    private static int clientTotal = 10000;//总共10000个请求

    private static volatile int count = 0;//获取到的数量

    public static void main(String[] args) throws InterruptedException {
        ExecutorService exec = Executors.newCachedThreadPool();//线程池
        final Semaphore semaphore = new Semaphore(threadTotal);//信号量
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int index = 0;index<clientTotal;index++){
            exec.execute(()->{
                try{
                    semaphore.acquire();
                    add();
                    semaphore.release();
                }catch (Exception e){
                    log.error("exception",e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        exec.shutdown();
        log.info("count:{}",count);
    }
    private static void add(){
        count++;
    }
}
4.5、volatile总结

直接使用volatile不是线程安全的,其不具有原子性,其不适合计数的场景

使用volatile的原则:

1、 对变量的写操作不依赖于当前值

2、 该变量没有包含在其它变量的不必要的式子中

3、 特别适合作为状态标记量 Boolean

//使用volatile:
volatile boolean inited = false;
//线程一:
context = loadContext();
inited = true;

//线程二:
while!inited){
    sleep();
}
doSomethingWithConfig(context);

五、有序性总结

5.1、为什么有有序性?

       Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

5.2、volatilesynchronizedlock都可以保证有序性。
5.3、happens-before原则【以下规则来自于《深入理解java虚拟机》】
  1. 程序次序规则:一个县城内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
  2. 锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。
  3. volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作。
  4. 传递规则:如果操作a先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C。

前四条很重要,后面的就很显而易见了

  1. 线程启动规则:Thread对象的stat()方法先行发生于此线程的每一个动作
  2. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
  3. 线程终结规则:线程中所有的操作都是先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  4. 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

六、线程安全指标总结

  • 原子性:Atomic包CAS算法synchronizedLock

    ​ 同一时刻只能有一个线程进行操作

  • 可见性:synchronizedvolatile

    ​ 一个线程对主内存的修改可以及时的被其他线程观察到

  • 有序性:happens-before

    ​ 一个线程观察其它线程的执行顺序,由于指令重排序的存在,这个观察结果一般都会杂乱无序。如果两个操作的执行顺序无法从happens-before原则中推导出来,那么他们就不能保证有序性,虚拟机可以随意的对他们进行重排序。

发布了20 篇原创文章 · 获赞 1 · 访问量 564

猜你喜欢

转载自blog.csdn.net/weixin_42295814/article/details/103775618