线程安全性详解(原子性、可见性、有序性)(1)

简介: 线程安全性详解(原子性、可见性、有序性)

一、定义:什么是线程安全

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

二、线程安全性的三个体现

原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作(Atomic、CAS算法、synchronized、Lock)

可见性:一个主内存的线程如果进行了修改,可以及时被其他线程观察到(synchronized、volatile)

有序性:如果两个线程不能从 happens-before原则 观察出来,那么就不能观察他们的有序性,虚拟机可以随意的对他们进行重排序,导致其观察观察结果杂乱无序(happens-before原则)

三、线程安全性:原子性

3.1、原子性 — Atomic包

在Java jdk中里面提供了很多Atomic类

AtomicXXX:CAS、Unsafe.compareAndSwapInt

AtomicLong、LongAdder

AtomicReference、AtomicReferenceFieldUpdater

AtomicStampReference:CAS的ABA问题

由于CAS原语的直接操作与计算机底层的联系很大,CAS原语有三个参数,内存地址、期望值、新值。我们在Java中一般不去直接写CAS相关的代码,JDK为我们封装在AtomicXXX中,因此,我们直接使用就可以了。


我们在 java.util.concurrent.atomic目录中可以看到我们这些类,包下提供了AtomicBoolean、AtomicLong、AtomicInteger三种原子更新基本类型和一个比较好玩的类AtomicReference,这些类都有一个共同点,都支持CAS,以AtomicInteger为重点讲解。

屏幕快照 2022-05-10 下午1.32.25.png

3.1.1、AtomicInteger

AtomicInteger是一个提供原子操作的Integer类,通过线程安全的方式操作加减

以下是AtomicIntege基本包括的方法:

public final int getAndSet(int newValue)       //给AtomicInteger设置newValue并返回加oldValue
public final boolean compareAndSet(int expect, int update)    //如果输入的值和期望值相等就set并返回true/false
public final int getAndIncrement()     //对AtomicInteger原子的加1并返回当前自增前的value
public final int getAndDecrement()   //对AtomicInteger原子的减1并返回自减之前的的value
public final int getAndAdd(int delta)   //对AtomicInteger原子的加上delta值并返加之前的value
public final int incrementAndGet()   //对AtomicInteger原子的加1并返回加1后的值
public final int decrementAndGet()    //对AtomicInteger原子的减1并返回减1后的值
public final int addAndGet(int delta)   //给AtomicInteger原子的加上指定的delta值并返回加后的值

示例:

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;
@Slf4j
public class AtomicIntegerExample {
    // 请求总数
    public static int clientTotal = 5000;
    // 同时并发执行的线程数
    public static int threadTotal = 200;
    public static AtomicInteger count = new AtomicInteger(0);
    public static void main(String[] args) throws Exception {
      //获取线程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        //定义信号量
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}", count.get());
    }
    private static void add() {
        count.incrementAndGet();
    }
}

这里我们使用请求总数为:5000,同时执行的并发线程数为:200,我们最终需要得到结果为:5000,这个执行结果才算正确。


查看返回结果:


13:43:26.473 [main] INFO com.mmall.concurrency.example.atomic.AtomicIntegerExample - count:5000

最后结果是 5000表示是线程安全的。

我们来看看 AtomicInteger底层代码中到底为我们做了什么?首先我们来看 AtomicInteger.incrementAndGet()方法

public class AtomicInteger extends Number implements java.io.Serializable{
/**
     *  对AtomicInteger原子的加1并返回加1后的值
     * @return 更新的值
     */
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
}

AtomicInteger调用了java底层的 unsafe.getAndAddInt()方法,这里是实现CAS 的关键。


incrementAndGet()是将自增后的值返回,还有一个方法getAndIncrement()是将自增前的值返回,分别对应++i和i++操作。同样的decrementAndGet()和getAndDecrement()则对--i和i--操作。


Unsafe类是在sun.misc包下,不属于Java标准。但是很多Java的基础类库,包括一些被广泛使用的高性能开发库都是基于

Unsafe类开发的,比如Netty、Cassandra、Hadoop、Kafka等。Unsafe类在提升Java运行效率,增强Java语言底层操作

能力方面起了很大的作用。Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针的问题。

过度的使用Unsafe类会使得出错的几率变大,因此Java官方并不建议使用的,官方文档也几乎没有。通常我们最好也不

要使用Unsafe类,除非有明确的目的,并且也要对它有深入的了解才行。

再来看 Unsafe.getAndAddInt()方法

  /*
   * 其中getIntVolatile和compareAndSwapInt都是native方法
   * getIntVolatile是获取当前的期望值
   * compareAndSwapInt就是我们平时说的CAS(compare and swap),通过比较如果内存区的值没有改变,那么就用新值直接给该内存区赋值
   */
    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;
    }
    public native int getIntVolatile(Object var1, long var2);
    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

我们可以看到getAndAddInt(Object var1, long var2, int var4),传进来的第一个参数是当前的一个对象,也就是我们的:count.incrementAndGet(),在getAndAddInt()中,var1就是count,var2就是当前的值,比如当前循环中count值为 2,var4为每次递增1


其次getAndAddInt()方法中涉及到的两个方法调用都定义为native,即java底层实现的本地方法


getIntVolatile():获取保存当前对象count的主存地址的引用(注意不是对象的值,是引用)。

compareAndSwapInt():比较当前对象的值和底层该对象的值是否相等,如果相等,则将当前对象值加1,如果不相等,则重新去获取底层该对象的值,这个方法的实现就是CPU的CAS(compare and swap)操作。

我们知道volatile具有一致性的特征,但是它不具备原子性,为什么AtomicInteger却同时具备一致性和原子性,原来在AtomicInteger源码中实现了这样一串代码:private volatile int value;,在AtomicInteger内部实现就使用了volatile关键字,这就是为什么执行CAS(对CAS有疑问的点击此处)操作的时候,从底层获取的数据就是最新的数据:


如果当前要保存的值和内存中最新的值不相等的话,说明在这个过程中被其他线程修改了,只

能获取更新当前值为最新值,再那这个当前值再去和重新去内存获取的最新值比较,直到二者

相等的时候,才完成+1的过程.

使用AtomicInteger的好处在于,它不同于sychronized关键字或lock用锁的形式来实现原子性,加锁会影响性能,而是采用循环比较的形式来提高性能。


3.1.2、AtomicLong


AtomicLong是作用是对长整形进行原子操作,依靠底层的cas来保障原子性的更新数据,在要添加或者减少的时候,会使用死循环不断地cas到特定的值,从而达到更新数据的目的。

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.AtomicLong;
@Slf4j
public class AtomicLongExample {
    // 请求总数
    public static int clientTotal = 5000;
    // 同时并发执行的线程数
    public static int threadTotal = 200;
    public static AtomicLong count = new AtomicLong(0);
    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}", count.get());
    }
    private static void add() {
        count.incrementAndGet();
        // count.getAndIncrement();
    }
}

执行结果:


14:59:38.978 [main] INFO com.mmall.concurrency.example.atomic.AtomicLongExample - count:5000

1

最后结果是 5000表示是线程安全的。


3.1.3、AtomicBoolean


AtomicBoolean位于java.util.concurrent.atomic包下,是java提供给的可以保证数据的原子性操作的一个类

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;
@Slf4j
public class AtomicBooleanExample {
    private static AtomicBoolean isHappened = new AtomicBoolean(false);
    // 请求总数
    public static int clientTotal = 5000;
    // 同时并发执行的线程数
    public static int threadTotal = 200;
    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    test();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("isHappened:{}", isHappened.get());
    }
    private static void test() {
        if (isHappened.compareAndSet(false, true)) {
            log.info("execute");
        }
    }
}

返回结果:


15:04:54.954 [pool-1-thread-2] INFO com.mmall.concurrency.example.atomic.AtomicBooleanExample - execute

15:04:54.971 [main] INFO com.mmall.concurrency.example.atomic.AtomicBooleanExample - isHappened:true


这里我们发现log.info("execute");,在代码中只执行了一次,并且isHappened:true的值为true,这是为啥呢?


这里是因为当程序第一次compareAndSet()的时候,使得isHappend变为了true,因为原子性的关系,没有其他线程进行干扰,通过使用AtomicBoolean,我们使某段代码只执行一次。


目录
相关文章
|
5月前
|
设计模式 安全 Java
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
88 1
|
5月前
|
缓存 Java 编译器
多线程内存模型问题之保证Java中的原子性,如何解决
多线程内存模型问题之保证Java中的原子性,如何解决
|
5月前
线程可见性和关键字volatile
线程可见性和关键字volatile
|
7月前
|
缓存 安全 Java
多线程的三大特性:原子性、可见性和有序性
多线程的三大特性:原子性、可见性和有序性
160 1
|
7月前
|
缓存 算法 Java
多线程04 死锁,线程可见性
多线程04 死锁,线程可见性
47 0
|
7月前
|
缓存 安全 Java
3.线程安全之可见性、有序性、原子性是什么?
3.线程安全之可见性、有序性、原子性是什么?
75 0
3.线程安全之可见性、有序性、原子性是什么?
|
7月前
|
Java
Java线程面试题:什么是原子性问题?如何解决?
Java线程面试题:什么是原子性问题?如何解决?
79 0
|
2月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
58 1
C++ 多线程之初识多线程
|
2月前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
27 3
|
2月前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
23 2