Java并发编程(四):线程安全性
一、定义
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
二、线程安全性
1)线程安全性的三个方面
- 原子性:提供了互斥访问,同一时刻只能有一个线程来对它进行操作。
- 可见性:一个线程对主内存的修改可以及时的被其它线程观察到。
- 有序性:一个线程观察其它线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序。
2)原子性-Atomic包

Java并发编程之CAS
1.AtomicXXX:CAS 、Unsafe.compareAndSwapInt
代码演示
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; /**
* 计数到 5000 , 代码运行结果 为 5000
*/
@Slf4j
public class CountExample2 { // 请求总数
public static int clientTotal = 5000; // 同时并发执行的线程数
public static int threadTotal = 200; public 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 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();
}
}
来看一下它的实现源码 , Atomic
在实现的时候, 使用了一个 unsafe
的类, unsafe
提供了一个 getAddAddInt
的方法 , 来看一下 这个方法的实现
incrementAndGet 方法实现
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
getAndAddInt 方法实现
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;
}
源码说明, 在这个源码实现里 , 用`do while 作为主体实现的 , 在 while 条件里 , 调用了一个核心的方法 compareAndSwapInt
在这个 getAndAddInt 方法里 , 传来的第一个参数是 请求的对象 , 就是上面示例代码里的 count ,第二个值是当前的值 ,比如当前执行的是 2 +1 这个操作, var2 就是 2 ,第三个参数 var4 的值是 1 , 而 var5 是底层当前的值. 如果没有其他线程对对象 count 进行操作, 其返回的底层的值应该是 2 , 此时, 当前值 2 和底层获取到的值 2 是相等的, compareAndSwapInt判断当前参数 var2 和底层var5的值相等, 则执行相加操作 将 底层获取的值 var5 加上 被加数var4 ,这个方法的最终目的就是, 对于这个传过来的 对象 count 如果底层的值和当前的值时相等的, 就将其更新为目标值 .
上面的代码的增加操作中, 在进行 2 + 1 操作的是时候, 对象 count 可能被其他线程更新, 当前值var2 就和 var5 不相等了, 所有就不能更新目标值 ,那么再次取出 底层的值 var5 , var2这个值再重新从当前对象 count 取一次, 再次判断是否符合更新要求 . 就是通过这样不停的循环, 当 var2 与 var5 完全相同的时候, 才进行更新值 . 这个 compareAndSwapInt 的核心 就是所谓的 CAS 的核心.
2.AtomicLong、LongAdder
Atomic 还有提供了 一个 类 AtomicLong , 其实现和 AtomicInteger 一样.
在Java 8 里 , Atomic 提供了 一个 LongAdder 类 , 上面通过看 CAS 底层实现的时候知道了, 它是通过一个死循环,不断的长沙市修改目标值 , 直到修改成功 , 如果并发不是很好的情况下, 修改成功的几率很高 , 如果大量修改失败, 这些原子操作就会进行多次的循环尝试, 因此性能会受到一定的影响. 这里有一个额外的知识点, 对于 long double 类型的变量, jvm 允许将 64 位的读操作者 或者写操作 拆分成两个 32 位的操作 。
LongAdder 这个类的设计 , 其核心是将热点数据分离, 比如它将 AtomicLong 内部的核心数据 value ,分离成一个数组 , 每个线程访问时候, 通过hash 等算法 ,将其映射到其中一个数字进行计数, 而最终的结果呢, 是这个数组的求和累加 . 热点数据value会被分离成多个的cell ,每个cell 独自维护内部的值, 当前对象的值由所有的cell 累计合成. 这样热点数据就进行了有效的分离 , 并提高了并行度 . 这样LongAdder 就相当于在 AtomicLong 基础上, 把单点的更新压力, 分散到各个节点上 。
LongAdder 在低并发的时候通过直接操作base,可以很好的保证和Atomic的性能基本一致,在高并发的场景,通过热点分区来提高并行度。
缺点:在统计的时候如果有并发更新,可能会导致结果有些误差
实际运用中:优先使用LongAdder ,在线程竞争很低的情况下使用AtomicLong效率更高
全局序列号使用AtomicLong。
3.AtomicReference、AtomicReferenceFieldUpdater
AtomicReference: 用法同AtomicInteger一样,但是可以放各种对象
@Slf4j@ThreadSafepublicclassAtomicExample4{publicstaticAtomicReference count =newAtomicReference<>(0);publicstaticvoidmain(String[] args)throwsInterruptedException{// 2count.compareAndSet(0,2);// nocount.compareAndSet(0,1);// nocount.compareAndSet(1,3);// 4count.compareAndSet(2,4);// nocount.compareAndSet(3,5); log.info("count:{}",count.get()); }}
AtomicReferenceFieldUpdater
@Slf4j@ThreadSafepublicclassAtomicExample5{@Getterprivatevolatileintcount =100;/**
* AtomicIntegerFieldUpdater 核心是原子性的去更新某一个类的实例的指定的某一个字段
* 构造函数第一个参数为类定义,第二个参数为指定字段的属性名,必须是volatile修饰并且非static的字段
*/privatestaticAtomicIntegerFieldUpdater updater = AtomicIntegerFieldUpdater.newUpdater(AtomicExample5.class,"count");publicstaticvoidmain(String[] args)throws InterruptedException{ AtomicExample5 example5 =newAtomicExample5();// 第一次 count=100 -> count->120 返回Trueif(updater.compareAndSet(example5,100,120)){log.info("update success 1:{}",example5.getCount()); }// count=120 -> 返回Falseif(updater.compareAndSet(example5,100,120)){log.info("update success 2:{}",example5.getCount()); }else{log.info("update field:{}",example5.getCount()); } }}
5.AtomicStampReference:CAS的ABA问题
ABA问题:在CAS操作的时候,其他线程将变量的值A改成了B由改成了A,本线程使用期望值A与当前变量进行比较的时候,发现A变量没有变,于是CAS就将A值进行了交换操作,这个时候实际上A值已经被其他线程改变过,这与设计思想是不符合的
解决思路:每次变量更新的时候,把变量的版本号加一,这样只要变量被某一个线程修改过,该变量版本号就会发生递增操作,从而解决了ABA变化
/** * Atomically sets the value of both the reference and stamp * to the given update values if the * current reference is {@code==} to the expected reference * and the current stamp is equal to the expected stamp. * *@paramexpectedReference the expected value of the reference *@paramnewReference the new value for the reference *@paramexpectedStamp the expected value of the stamp(上面提到的版本号) *@paramnewStamp the new value for the stamp *@return{@codetrue} if successful */publicbooleancompareAndSet(V expectedReference, V newReference,intexpectedStamp,intnewStamp){ Pair current = pair;returnexpectedReference == current.reference && expectedStamp == current.stamp && ((newReference == current.reference && newStamp == current.stamp) || casPair(current, Pair.of(newReference, newStamp))); }
6.AtomicLongArray
可以指定更新一个数组指定索引位置的值
/** * Atomically sets the element at position {@codei} to the given value * and returns the old value. * *@parami the index *@paramnewValue the new value *@returnthe previous value */publicfinallonggetAndSet(inti,longnewValue){returnunsafe.getAndSetLong(array, checkedByteOffset(i), newValue); }....../** * Atomically sets the element at position {@codei} to the given * updated value if the current value {@code==} the expected value. * *@parami the index *@paramexpect the expected value *@paramupdate the new value *@return{@codetrue} if successful. False return indicates that * the actual value was not equal to the expected value. */publicfinalbooleancompareAndSet(inti,longexpect,longupdate){returncompareAndSetRaw(checkedByteOffset(i), expect, update); }
7.AtomicBoolean(平时用的比较多)
compareAndSet方法也值得注意,可以达到同一时间只有一个线程执行这段代码
/** * Atomically sets the value to the given updated value * if the current value {@code==} the expected value. * *@paramexpect the expected value *@paramupdate the new value *@return{@codetrue} if successful. False return indicates that * the actual value was not equal to the expected value. */publicfinalbooleancompareAndSet(booleanexpect,booleanupdate){inte = expect ?1:0;intu = update ?1:0;returnunsafe.compareAndSwapInt(this, valueOffset, e, u); }
8.原子性-锁
- synchronized:依赖JVM (主要依赖JVM实现锁,因此在这个关键字作用对象的作用范围内,都是同一时刻只能有一个线程进行操作的)
- Lock:依赖特殊的CPU指令,代码实现,ReentrantLock
修饰的内容分类
- 修饰代码块:大括号括起来的代码,作用于调用的对象
- 修饰方法:整个方法,作用于调用的对象
- 修饰静态方法:整个静态方法,作用于所有对象
- 修饰类:括号括起来的部分,作用于所有对象
3)可见性
导致共享变量在线程间不可见的原因
1、线程的交叉执行
2、重排序结合线程交叉执行
3、共享变量更新后的值没有在工作内存与主内存间及时更新
可见性-synchronized
JMM关于synchronized的两条规定:
1、线程解锁前,必须把共享变量的最新值刷新到主内存
2、线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从
主内存中重新读取最新的值(注意:加锁与解锁是同一把锁)
更多理解:关于java多线程中synchronized关键字的理解
可见性-volatile
通过加入内存屏障和禁止重排序优化来实现
1、对volatile变量写操作时,会在写操作后加入一条store屏障指令,将本地内存中的
共享变量值刷新到主内存
2、对volatile变量读操作时,会在读操作前加入一条load屏障指令,从主内存读取

更多volatile知识:关于java多线程关键字volatile的理解
4)有序性
一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序。
JMM允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
可以通过volatile、synchronized、lock保证有序性。
Happens-before原则
先天有序性,即不需要任何额外的代码控制即可保证有序性,java内存模型一个列出了八种Happens-before规则,如果两个操作的次序不能从这八种规则中推倒出来,则不能保证有序性。
程序次序规则:一个线程内,按照代码执行,书写在前面的操作先行发生于书写在后面的操作。
锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作
volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
线程启动原则:Thread对象的start()方法先行发生于此线程的每一个动作
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()方法返回值手段检测到线程已经终止执行
对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
第一条规则要注意理解,这里只是程序的运行结果看起来像是顺序执行,虽然结果是一样的,jvm会对没有变量值依赖的操作进行重排序,这个规则只能保证单线程下执行的有序性,不能保证多线程下的有序性。
Java并发编程(四):线程安全性的更多相关文章
- Java并发编程 (四) 线程安全性
个人博客网:https://wushaopei.github.io/ (你想要这里多有) 一.线程安全性-原子性-atomic-1 1.线程安全性 定义: 当某个线程访问某个类时,不管运行时环境 ...
- JAVA并发编程之线程安全性
1.一个对象是否是线程安全的,取决于它是否被多个线程访问.想要使得线程安全,需要通过同步机制来协同对对象可变状态的访问. 2.修复多线程访问可变状态变量出现的错误:1.程序间不共享状态变量 2.状态变 ...
- Java并发编程 (五) 线程安全性
个人博客网:https://wushaopei.github.io/ (你想要这里多有) 一.安全发布对象-发布与逸出 1.发布与逸出定义 发布对象 : 使一个对象能够被当前范围之外的代码所使用 ...
- Java并发编程:线程池的使用
Java并发编程:线程池的使用 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了, ...
- Java并发编程:线程池的使用(转)
Java并发编程:线程池的使用 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了, ...
- Java并发编程:线程控制
在上一篇文章中(Java并发编程:线程的基本状态)我们介绍了线程状态的 5 种基本状态以及线程的声明周期.这篇文章将深入讲解Java如何对线程进行状态控制,比如:如何将一个线程从一个状态转到另一个状态 ...
- Java 并发编程:线程间的协作(wait/notify/sleep/yield/join)
Java并发编程系列: Java 并发编程:核心理论 Java并发编程:Synchronized及其实现原理 Java并发编程:Synchronized底层优化(轻量级锁.偏向锁) Java 并发编程 ...
- Java并发编程:线程池的使用(转载)
转载自:https://www.cnblogs.com/dolphin0520/p/3932921.html Java并发编程:线程池的使用 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实 ...
- Java并发编程:线程池的使用(转载)
文章出处:http://www.cnblogs.com/dolphin0520/p/3932921.html Java并发编程:线程池的使用 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实 ...
- [转]Java并发编程:线程池的使用
Java并发编程:线程池的使用 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了, ...
随机推荐
- HDU 4417 Super Mario(划分树)
Super Mario Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)Total ...
- JavaScript 巧学巧用
关于 微信公众号:前端呼啦圈(Love-FED) 我的博客:劳卜的博客 知乎专栏:前端呼啦圈 前言 由于工作和生活上的一些变化,最近写文章的频率有点下降了,实在不好意思,不过相信不久就会慢慢恢复过来, ...
- mongo 误操作恢复数据
场景:我往同一个集合里面插入 三条数据 aa:aa bb:bb cc:cc .后来我后悔了,不想插入 bb:bb,通过oplog重放过滤好 bb:bb这条数据. 原理: 1.通过 oplog.r ...
- 机器学习-Confusion Matrix混淆矩阵、ROC、AUC
本文整理了关于机器学习分类问题的评价指标——Confusion Matrix.ROC.AUC的概念以及理解. 混淆矩阵 在机器学习领域中,混淆矩阵(confusion matrix)是一种评价分类模型 ...
- Hibernate简介与实例
一.Hibernate简介 1.什么是Hibernate? Hibernate是数据持久层的一个轻量级框架.数据持久层的框架有很多比如:iBATIS,myBatis,Nhibernate,Siena等 ...
- Spark Streaming的PIDRateEstimator与backpressure
PIDRateEstimator是Spark Streaming用来实现backpressure的关键组件. 看了一些博客文章,感觉对它的解释都没有说到要点,还是自己来研究一下比较好. 首先,需要搞清 ...
- wireshark在windows下无法抓取localhost数据包
在调试SSL时要抓包,通过tcpview和minisniffer等工具明明看到tcp连接已经建立并开始收发数据了,但wireshark却总是无法抓到相应的数据包. 今天早上,HQ的高工告诉我“wire ...
- C# this.Hide()
C# this.Hide() 第一次用的时候是在_Load函数里: BookSystem bs = new BookSystem(); bs.ShowDialog(); ...
- Microsoft.VisualStudio.Shell.14.0.dll 文件位置
"C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\IDE\CommonExtensions\Platform\Shel ...
- 一般源码安装添加的GD库 是不支持 jpeg 格式的图片的
一般源码安装添加的GD库 是不支持 jpeg 格式的图片的,只支持如下格式 GD Support enabled GD Version bundled (2.0.34 compatible) GIF ...