文章目录
- 一、线程安全性的定义
- 二、线程安全性指标
- 1.1、原子性
- 1.2、可见性
- 1.3、有序性
- 三、原子性总结
- 2.1、使用具有原子性的操作
- 2.2、`Atomic`包
- 2.2.1、`AtomicInteger`的`incrementAndGet()`方法具备原子性
- 2.2.2、`LongAdder` 解析
- 2.2.3、`AtomicReference` 与 `AtomicReferenceFieldUpdater` 的使用
- 2.2.4、`AtomicStampReference`与 `CAS` 的 `ABA` 问题
- 2.2.5、`AtomicBoolean` 的使用
- 2.3、原子性-锁
- 2.3.1、`synchronized`
- 2.3.2、Lock
- 2.3.3、原子性对比
- 四、可见性总结
- 4.1、导致共享变量在线程间不可见的原因
- 4.2、JMM关于`synchronized`的两条规定:
- 4.3、可见性 – `volatile`
- 4.4、使用volatile测试其原子性(不具备原子性)
- 4.5、`volatile`总结
- 五、有序性总结
- 5.1、为什么有有序性?
- 5.2、`volatile`、`synchronized`、`lock`都可以保证有序性。
- 5.3、`happens-before`原则【以下规则来自于《深入理解java虚拟机》】
- 六、线程安全指标总结
一、线程安全性的定义
当多个线程访问某个类时,不管运行时环境采用何种调度方式,或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类时线程安全的。
二、线程安全性指标
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、AtomicInteger
的incrementAndGet()
方法具备原子性
- 在测试中通过
CountDownLatch
的countDown()
方法 ( 执行完一次就减一 ) 和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;
}
AtomicLong
和AtomicInteger
是一样的效果,一样的使用。
2.2.2、LongAdder
解析
- JDK8新类,线程高的情况优先使用
- 为什么有了
AtomicLong
还要新增一个LongAdder
呢?- CAS底层实现是在一个死循环中不断地尝试修改目标值,直到修改成功。如果竞争不激烈的时候,修改成功率很高,否则失败率很高。在失败的时候,这些重复的原子性操作会耗费性能。
- 对于普通类型的
long
、double
变量,JVM
允许将64位的读操作或写操作拆成两个32位的操作。LongAdder
在AtomicLong
的基础上将单点的更新压力分散到各个节点,在低并发的时候通过对base
的直接更新可以很好的保障和AtomicLong
的性能基本保持一致,而在高并发的时候通过分散提高了性能。- 缺点是
LongAdder
在统计的时候如果有并发更新,可能导致统计的数据有误差。
2.2.3、AtomicReference
与 AtomicReferenceFieldUpdater
的使用
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、AtomicStampReference
与 CAS
的 ABA
问题
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中的一个关键字,是一种同步锁。它可以修饰的对象主要有四种:
- 修饰代码块:大括号括起来的代码,作用于调用的对象
- 修饰方法:整个方法,作用于调用的对象
———————————————————————–- 修饰静态方法:整个静态方法,作用于所有对象
- 修饰类:括号括起来的部分,作用于所有对象
依赖特殊的CPU指令,代码实现,ReentrantLock
2.3.3、原子性对比
synchronized
:不可中断锁,适合竞争不激烈,竞争激烈时性能下降特别快。(经常使用,面试经常被问)Lock
:可中断锁,多样化同步,竞争激烈时能维持常态。Atomic
:竞争激烈时能维持常态,比Lock
性能好;只能同步一个值。(经常使用,面试经常被问)
四、可见性总结
4.1、导致共享变量在线程间不可见的原因
- 线程交叉执行
- 重排序结合线程交叉执行
- 共享变量更新后的值没有在工作内存与主存间及时更新(Java内存模型会了这个就理解了)
4.2、JMM关于synchronized
的两条规定:
- 线程解锁前,必须把共享变量的最新值刷新到主内存
- 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意,加锁与解锁是同一把锁)
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、volatile
、synchronized
、lock
都可以保证有序性。
5.3、happens-before
原则【以下规则来自于《深入理解java虚拟机》】
- 程序次序规则:一个县城内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
- 锁定规则:一个
unlock
操作先行发生于后面对同一个锁的lock
操作。volatile
变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作。- 传递规则:如果操作a先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C。
前四条很重要,后面的就很显而易见了
- 线程启动规则:
Thread
对象的stat()
方法先行发生于此线程的每一个动作- 线程中断规则:对线程
interrupt()
方法的调用先行发生于被中断线程的代码检测到中断事件的发生。- 线程终结规则:线程中所有的操作都是先行发生于线程的终止检测,我们可以通过
Thread.join()
方法结束、Thread.isAlive()
的返回值手段检测到线程已经终止执行- 对象终结规则:一个对象的初始化完成先行发生于他的
finalize()
方法的开始
六、线程安全指标总结
- 原子性:
Atomic包
、CAS算法
、synchronized
、Lock
同一时刻只能有一个线程进行操作- 可见性:
synchronized
、volatile
一个线程对主内存的修改可以及时的被其他线程观察到- 有序性:
happens-before
一个线程观察其它线程的执行顺序,由于指令重排序的存在,这个观察结果一般都会杂乱无序。如果两个操作的执行顺序无法从happens-before
原则中推导出来,那么他们就不能保证有序性,虚拟机可以随意的对他们进行重排序。