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

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

 [[399624]]

本文转载自微信公众号「牧小农」,作者牧小农。转载本文请联系牧小农公众号。

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

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

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

原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作(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为重点讲解。

3.1.1、AtomicInteger

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

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

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

示例:

  1. import lombok.extern.slf4j.Slf4j; 
  2.  
  3. import java.util.concurrent.CountDownLatch; 
  4. import java.util.concurrent.ExecutorService; 
  5. import java.util.concurrent.Executors; 
  6. import java.util.concurrent.Semaphore; 
  7. import java.util.concurrent.atomic.AtomicInteger; 
  8.  
  9. @Slf4j 
  10. public class AtomicIntegerExample { 
  11.  
  12.     // 请求总数 
  13.     public static int clientTotal = 5000; 
  14.  
  15.     // 同时并发执行的线程数 
  16.     public static int threadTotal = 200; 
  17.  
  18.     public static AtomicInteger count = new AtomicInteger(0); 
  19.  
  20.     public static void main(String[] args) throws Exception { 
  21.       //获取线程池 
  22.         ExecutorService executorService = Executors.newCachedThreadPool(); 
  23.         //定义信号量 
  24.         final Semaphore semaphore = new Semaphore(threadTotal); 
  25.         final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); 
  26.         for (int i = 0; i < clientTotal ; i++) { 
  27.             executorService.execute(() -> { 
  28.                 try { 
  29.                     semaphore.acquire(); 
  30.                     add(); 
  31.                     semaphore.release(); 
  32.                 } catch (Exception e) { 
  33.                     log.error("exception", e); 
  34.                 } 
  35.                 countDownLatch.countDown(); 
  36.             }); 
  37.         } 
  38.         countDownLatch.await(); 
  39.         executorService.shutdown(); 
  40.         log.info("count:{}"count.get()); 
  41.     } 
  42.  
  43.     private static void add() { 
  44.         count.incrementAndGet(); 
  45.     } 

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

查看返回结果:

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

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

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

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

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

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

  1. Unsafe类是在sun.misc包下,不属于Java标准。但是很多Java的基础类库,包括一些被广泛使用的高性能开发库都是基于 
  2. Unsafe类开发的,比如Netty、Cassandra、Hadoop、Kafka等。Unsafe类在提升Java运行效率,增强Java语言底层操作 
  3. 能力方面起了很大的作用。Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针的问题。 
  4. 过度的使用Unsafe类会使得出错的几率变大,因此Java官方并不建议使用的,官方文档也几乎没有。通常我们最好也不 
  5. 要使用Unsafe类,除非有明确的目的,并且也要对它有深入的了解才行。 

再来看 Unsafe.getAndAddInt()方法

  1. /* 
  2.    * 其中getIntVolatile和compareAndSwapInt都是native方法 
  3.    * getIntVolatile是获取当前的期望值 
  4.    * compareAndSwapInt就是我们平时说的CAS(compare and swap),通过比较如果内存区的值没有改变,那么就用新值直接给该内存区赋值 
  5.    */ 
  6.     public final int getAndAddInt(Object var1, long var2, int var4) { 
  7.         int var5; 
  8.         do { 
  9.             var5 = this.getIntVolatile(var1, var2); 
  10.         } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); 
  11.         return var5; 
  12.     } 
  13.      
  14.     public native int getIntVolatile(Object var1, long var2); 
  15.     public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5); 

我们可以看到 getAndAddInt(Objectvar1,longvar2,intvar4),传进来的第一个参数是当前的一个对象,也就是我们的: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源码中实现了这样一串代码:privatevolatileintvalue;,在 AtomicInteger内部实现就使用了 volatile关键字,这就是为什么执行CAS(对CAS有疑问的点击此处)操作的时候,从底层获取的数据就是最新的数据:

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

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

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

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

3.1.2、AtomicLong

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

  1. import lombok.extern.slf4j.Slf4j; 
  2.  
  3. import java.util.concurrent.CountDownLatch; 
  4. import java.util.concurrent.ExecutorService; 
  5. import java.util.concurrent.Executors; 
  6. import java.util.concurrent.Semaphore; 
  7. import java.util.concurrent.atomic.AtomicLong; 
  8.  
  9. @Slf4j 
  10. public class AtomicLongExample { 
  11.  
  12.     // 请求总数 
  13.     public static int clientTotal = 5000; 
  14.  
  15.     // 同时并发执行的线程数 
  16.     public static int threadTotal = 200; 
  17.  
  18.     public static AtomicLong count = new AtomicLong(0); 
  19.  
  20.     public static void main(String[] args) throws Exception { 
  21.         ExecutorService executorService = Executors.newCachedThreadPool(); 
  22.         final Semaphore semaphore = new Semaphore(threadTotal); 
  23.         final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); 
  24.         for (int i = 0; i < clientTotal ; i++) { 
  25.             executorService.execute(() -> { 
  26.                 try { 
  27.                     semaphore.acquire(); 
  28.                     add(); 
  29.                     semaphore.release(); 
  30.                 } catch (Exception e) { 
  31.                     log.error("exception", e); 
  32.                 } 
  33.                 countDownLatch.countDown(); 
  34.             }); 
  35.         } 
  36.         countDownLatch.await(); 
  37.         executorService.shutdown(); 
  38.         log.info("count:{}"count.get()); 
  39.     } 
  40.  
  41.     private static void add() { 
  42.         count.incrementAndGet(); 
  43.         // count.getAndIncrement(); 
  44.     } 

执行结果:

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

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

3.1.3、AtomicBoolean

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

  1. import lombok.extern.slf4j.Slf4j; 
  2.  
  3. import java.util.concurrent.CountDownLatch; 
  4. import java.util.concurrent.ExecutorService; 
  5. import java.util.concurrent.Executors; 
  6. import java.util.concurrent.Semaphore; 
  7. import java.util.concurrent.atomic.AtomicBoolean; 
  8.  
  9. @Slf4j 
  10. public class AtomicBooleanExample { 
  11.  
  12.     private static AtomicBoolean isHappened = new AtomicBoolean(false); 
  13.  
  14.     // 请求总数 
  15.     public static int clientTotal = 5000; 
  16.  
  17.     // 同时并发执行的线程数 
  18.     public static int threadTotal = 200; 
  19.  
  20.     public static void main(String[] args) throws Exception { 
  21.         ExecutorService executorService = Executors.newCachedThreadPool(); 
  22.         final Semaphore semaphore = new Semaphore(threadTotal); 
  23.         final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); 
  24.         for (int i = 0; i < clientTotal ; i++) { 
  25.             executorService.execute(() -> { 
  26.                 try { 
  27.                     semaphore.acquire(); 
  28.                     test(); 
  29.                     semaphore.release(); 
  30.                 } catch (Exception e) { 
  31.                     log.error("exception", e); 
  32.                 } 
  33.                 countDownLatch.countDown(); 
  34.             }); 
  35.         } 
  36.         countDownLatch.await(); 
  37.         executorService.shutdown(); 
  38.         log.info("isHappened:{}", isHappened.get()); 
  39.     } 
  40.  
  41.     private static void test() { 
  42.         if (isHappened.compareAndSet(falsetrue)) { 
  43.             log.info("execute"); 
  44.         } 
  45.     } 

返回结果:

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

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

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

3.1.4、AtomicReference

AtomicReference和 AtomicInteger非常类似,不同之处就在于 AtomicInteger是对整数的封装,底层采用的是 compareAndSwapInt实现CAS,比较的是数值是否相等,而 AtomicReference则对应普通的对象引用,底层使用的是 compareAndSwapObject实现CAS,比较的是两个对象的地址是否相等。也就是它可以保证你在修改对象引用时的线程安全性。

多个线程之间的操作无论采用何种执行时序或交替方式,都要保证不变性条件不被破坏,要保持状态的一致性,就需要在单个原子操作中更新相关的状态变量。

  1. import lombok.extern.slf4j.Slf4j; 
  2.  
  3. import java.util.concurrent.atomic.AtomicReference; 
  4.  
  5. @Slf4j 
  6. public class AtomicReferenceExample { 
  7.  
  8.     private static AtomicReference<Integercount = new AtomicReference<>(0); 
  9.  
  10.     public static void main(String[] args) { 
  11.         count.compareAndSet(0, 2);  
  12.         count.compareAndSet(0, 1); 
  13.         count.compareAndSet(1, 3);  
  14.         count.compareAndSet(2, 4);  
  15.         count.compareAndSet(3, 5);  
  16.         log.info("count:{}"count.get()); 
  17.     } 

大家觉得我们输出的结果会是多少?

返回结果:

  1. 15:26:59.680 [main] INFO com.mmall.concurrency.example.atomic.AtomicReferenceExample - count:4 

为什么是4呢?首先我们 要说的是 publicfinalbooleancompareAndSet(V expect,V update)这个方法,这个方法主要的作用是通过比对两个对象,然后更新为新的对象,这里的比对两个对象,比对的方式不是 equals而是 ==,意味着比对的是内存的中地址。

1、首先我们创建 count的初始化为0 2、在main方法中 count.compareAndSet(0,2);,判断count为0时赋值为2 3、在 count.compareAndSet(0,1);和 count.compareAndSet(1,3);判断count是否为1或者0,因为上一步我们已经赋值为2了,所以判断不成立 4、在 count.compareAndSet(2,4);判断count是否为2,等式成立 5、最好输出结果为4

  1. count.compareAndSet(0, 2); //count=0?赋值 2,判断成立,此时count=0,更新后为2 
  2. count.compareAndSet(0, 1); //count=0?赋值 1,判断不成立,此时count=2 
  3. count.compareAndSet(1, 3); //count=1?赋值 3,判断不成立,此时count=2 
  4. count.compareAndSet(2, 4); //count=2?赋值 4,判断成立,此时count=2,更新后count=4 
  5. count.compareAndSet(3, 5); //count=3?赋值 5,判断不成立,此时count=4 

所以我们输出结果为:4

3.1.5、CAS中ABA问题的解决

CAS并非完美的,它会导致ABA问题,例如:当前内存的值一开始是A,被另外一个线程先改为B然后再改为A,那么当前线程访问的时候发现是A,则认为它没有被其他线程访问过。在某些场景下这样是存在错误风险的。比如在链表中。如何解决这个ABA问题呢,大多数情况下乐观锁的实现都会通过引入一个版本号标记这个对象,每次修改版本号都会变话,比如使用时间戳作为版本号,这样就可以很好的解决ABA问题。在JDK中提供了 AtomicStampedReference类来解决这个问题,这个类维护了一个int类型的标记stamp,每次更新数据的时候顺带更新一下stamp。

3.2、原子性 — synchronized

synchronized是一种同步锁,通过锁实现原子操作。1、修饰代码块:大括号括起来的代码,作用于调用的对象 2、修饰方法:整个方法,作用于调用的对象 3、修饰静态方法:整个静态方法,作用于所有对象 4、修饰类:括号括起来的部分,作用于所有对象

详细可以查看,我写的关于:synchronized的博客,因为写过所以就不做过多描述。

3.3、原子性 — 对比

Atomic:竞争激烈时能维持常态,比Lock性能好, 只能同步一个值

synchronized:不可中断锁,适合竞争不激烈,可读性好的情况

Lock:可中断锁,多样化同步,竞争激烈时能维持常态

四、线程安全性:可见性

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

导致共享变量在线程间不可见的原因:1.线程交叉执行 2.重新排序结合线程交叉执行 3.共享变量更新后的值没有在工作内存中与主内存间及时更新

4.1 可见性 — syncronized

JMM关于 syncronized的两条规定:

线程解锁前,必须把共享变量的最新值刷新到主内存中

线程加锁时,将清空工作内存中共享变量的值,从而使得使用共享变量时需要从主内存中重新读取最新的值(注意:加锁与解锁是同一把锁) 由于syncronized可以保证原子性及可见性,变量只要被syncronized修饰,就可以放心的使用

4.2 可见性 — volatile

通过加入 内存屏障和 禁止重排序优化来实现可见性。具体实现过程:

  • 对 volatile变量写操作时,会在写操作后加入一条 store屏障指令,将本地内存中的共享变量值刷新到主内存
  • 对 volatile变量读操作时,会在读操作前加入一条 load屏障指令,从主内存中读取共享变量
  • volatile 不能保证操作的原子性,也就是不能保证线程安全性, 如果需要使用 volatile 必须满足以下两个条件:
  • 对变量的写操作不依赖与变量当前的值。
  • 该变量没有包含在具有其他变量的不变的式子中。

所以 volatile修饰的变量适合作为状态标记量。

注:以下图片为资料中获取,如有雷同,纯属巧合

示例:

  1. import lombok.extern.slf4j.Slf4j; 
  2.  
  3. import java.util.concurrent.CountDownLatch; 
  4. import java.util.concurrent.ExecutorService; 
  5. import java.util.concurrent.Executors; 
  6. import java.util.concurrent.Semaphore; 
  7.  
  8. @Slf4j 
  9. public class VolatileExample { 
  10.  
  11.     // 请求总数 
  12.     public static int clientTotal = 5000; 
  13.  
  14.     // 同时并发执行的线程数 
  15.     public static int threadTotal = 200; 
  16.  
  17.     public static volatile int count = 0; 
  18.  
  19.     public static void main(String[] args) throws Exception { 
  20.         ExecutorService executorService = Executors.newCachedThreadPool(); 
  21.         final Semaphore semaphore = new Semaphore(threadTotal); 
  22.         final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); 
  23.         for (int i = 0; i < clientTotal ; i++) { 
  24.             executorService.execute(() -> { 
  25.                 try { 
  26.                     semaphore.acquire(); 
  27.                     add(); 
  28.                     semaphore.release(); 
  29.                 } catch (Exception e) { 
  30.                     log.error("exception", e); 
  31.                 } 
  32.                 countDownLatch.countDown(); 
  33.             }); 
  34.         } 
  35.         countDownLatch.await(); 
  36.         executorService.shutdown(); 
  37.         log.info("count:{}"count); 
  38.     } 
  39.  
  40.     private static void add() { 
  41.         count++; 
  42.     } 

返回结果:

  1. 16:12:01.404 [main] INFO com.mmall.concurrency.example.count.VolatileExample4 - count:4986 

通过执行代码我们可以发现,返回结果并不是我们想看到的5000,说明这个是线程不安全的类

主要是因为当我们执行 conut++时分成了三步:1、取出当前内存count值,这时count值时最新的 2、+1操作 3、重新写回主存

例如:有两个线程同时在执行 count++,两个内存都执行了第一步,比如当前count值为99,它们都读到了这个count值,然后两个线程分别执行了 +1,并写回主存,这样就丢掉了一次 +1的操作。

五、线程安全性:有序性

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

通过volatile、synchronized、lock保证有序性

5.1 happens-before原则

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  • 锁定规则:一个 unLock操作先行发生于后面对同一个锁的 Lock()操作,也就是说只有先解锁才能对下面的线程进行加锁
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  • 传递规则:如果操作A先行发生与操作B,而操作B先行发生于操作C,则操作A先行发生于操作C
  • 线程启动规则: Thread对象的 start()方法先行发生于此线程的每一个动作,一个线程只有执行了 start()方法后才能做其他的操作
  • 线程终端规则:对线程 interrupt()方法的调用先行发生与被中断线程的代码检测到中断事件的发生(只有执行了 interrupt()方法才可以检测到中断事件的发生)
  • 线程终结规则:线程中所有操作都先行发生于线程的终止检测,我们可以通过 Thread.join()方法结束, Thread.isAlive()的返回值手段检测到线程已经终止执行
  • 对象终结规则:一个对象的初始化完成先行发生于他的 finalize()方法的开始

六、线程安全性:总结

  • 原子性:Atomic包、CAS算法、synchronized、Lock
  • 可见性:synchronized、volatile

一个主内存的线程如果进行了修改,可以及时被其他线程观察到,介绍了volatile如何被观察到的

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

 

责任编辑:武晓燕 来源: 牧小农
相关推荐

2021-01-12 07:39:48

线程线程安全

2021-05-06 19:20:05

Java内存模型

2024-02-27 17:46:25

并发程序CPU

2024-11-18 16:37:35

JMMJava内存模型

2021-07-03 17:44:34

并发高并发原子性

2023-05-17 08:52:56

Java原子性可见性

2009-11-30 09:41:38

2020-03-04 08:25:18

有序性并发结构

2012-12-26 10:53:26

2024-02-26 08:33:51

并发编程活跃性安全性

2024-05-13 08:51:39

2016-09-19 21:53:30

Java并发编程解析volatile

2020-07-14 10:59:39

云计算云安全技术

2015-06-15 10:48:25

2013-04-24 10:31:44

公有云云安全

2009-10-23 10:50:04

CLR安全性

2021-06-03 14:00:35

PolarDB

2021-06-02 16:30:33

PolarDB原子性数据库

2023-01-05 12:30:32

Redis

2011-01-04 16:20:26

linux安全性
点赞
收藏

51CTO技术栈公众号