文章目录
概述:所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行。
引出问题(代码示例)
class VolatileAtomicThread implements Runnable {
// 定义一个int类型的遍历
private int count = 0 ;
@Override
public void run() {
// 对该变量进行++操作,100次
for(int x = 1 ; x <=100 ; x++) {
count++ ;
System.out.println("count =========>>>> " + count);
}
}
}
public class VolatileAtomicThreadDemo {
public static void main(String[] args) {
// 创建VolatileAtomicThread对象
VolatileAtomicThread volatileAtomicThread = new VolatileAtomicThread() ;
// 开启100个线程对count进行++操作
for(int x = 1 ; x <=100 ; x++) {
new Thread(volatileAtomicThread).start();
}
}
}
- 执行结果:不保证一定是10000
问题原理说明
- 上面的问题主要是发生在count++操作上:
- count++操作包含3个步骤:
- 从主内存中读取数据到工作内存
- 对工作内存中的数据进行++操作
- 将工作内存中的数据写回到主内存
- count++操作不是一个原子性操作,也就是说在某一个时刻对某一个操作的执行,有可能被其他的线程打断。
- 1)假设此时x的值是100,线程A需要对该变量进行自增1的操作,首先它需要从主内存中读取变量x的值。由于CPU的切换关系,此时CPU的执行权被切换到了B线程。A线程就处于就绪状态,B线程处于运行状态
- 2)线程B也需要从主内存中读取x变量的值,由于线程A没有对x值做任何修改。因此此时B读取到的数据还是100
- 3)线程B工作内存中x执行了+1操作,但是未刷新之主内存中
- 4)此时CPU的执行权切换到了A线程上,由于此时线程B没有将工作内存中的数据刷新到主内存,因此A线程工作内存中的变量值还是100,没有失效。A线程对工作内存中的数据进行了+1操作
- 5)线程B将101写入到主内存
- 6)线程A将101写入到主内存
虽然计算了2次,但是只对A进行了1次修改。那么,这个时候问题就出现,我们怎么解决呢?
问题解决
- 如何保证变量访问的原子性呢?
- 1.加锁实现线程安全。
- 2.基于CAS方式的原子类。
方法1:使用锁机制
- 我们可以给count++操作添加锁,那么count++操作就是临界区的代码,临界区只能有一个线程去执行,所以count++就变成了原子操作。
for(int x = 1 ; x <=100 ; x++) {
synchronized(this){
count++ ;
System.out.println("count =========>>>> " + count);
}
}
方法2:原子类
概述:java从JDK1.5开始提供了java.util.concurrent.atomic包(简称Atomic包),这个包中的原子操作类提供了一种用法简单,性能高效,线程安全地更新一个变量的方式。
AtomicInteger
- 原子型Integer,可以实现原子更新操作
public AtomicInteger(): 初始化一个默认值为0的原子型Integer
public AtomicInteger(int initialValue): 初始化一个指定值的原子型Integer
int get(): 获取值
int getAndIncrement(): 以原子方式将当前值加1,注意,这里返回的是自增前的值。
int incrementAndGet(): 以原子方式将当前值加1,注意,这里返回的是自增后的值。
int addAndGet(int data): 以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。
int getAndSet(int value): 以原子方式设置为newValue的值,并返回旧值。
- 代码示例
class VolatileAtomicThread implements Runnable {
//原子类中封装好了整型变量,默认值是0
private AtomicInteger atomicInteger = new AtomicInteger();
@Override
public void run() {
// 对该变量进行++操作,100次
for(int x = 1 ; x <=100 ; x++) {
int i = atomicInteger.incrementAndGet();
System.out.println("count =========>>>> " + i);
}
}
}
原子类CAS机制实现线程安全
概述
- CAS的全称是:
Compare And Swap
(比较再交换); 是现代CPU广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。CAS可以将read-modify-check-write
转换为原子操作
,这个原子操作直接由处理器保证。 - CAS机制当中使用了3个基本操作数:
内存地址V,旧的预期值A,要修改的新值B。
源码分析
int i = atomicInteger.incrementAndGet(); 底层变量+1且返回!
public final int incrementAndGet() {
//valueOffset:当前atomicInteger对象中值的偏移量,近似的认为地址值。
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); //获取当前atomicInteger对象中的值
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
//(CAS:比较并交换) var1:当前int值, var2:当前值的地址, var5:当前int值, var5 + var4:预测增加后的值
return var5;
}
CAS与Synchronized:乐观锁,悲观锁。
- CAS和Synchronized都可以保证多线程环境下共享数据的安全性。那么他们两者有什么区别?
Synchronized是从悲观的角度出发(悲观锁)
:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。因此Synchronized我们也将其称之为悲观锁。jdk中的ReentrantLock也是一种悲观锁。性能较差。CAS是从乐观的角度出发(乐观锁)
:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。CAS这种机制我们也可以将其称之为乐观锁。综合性能较好!但是,CAS又会引出ABA问题
,我们可以思考一下怎么解决,后面我会专门出一篇博客的。
你知道的越多,你不知道的越多。
有道无术,术尚可求,有术无道,止于术。
如有其它问题,欢迎大家留言,我们一起讨论,一起学习,一起进步