文章目录
可见性问题:让一个线程对共享变量的修改,能够及时的被其他线程看到。
- 根据 JMM 中规定的happen before 和 同步原则:对某个volatile字段的写操作 happens-before 每个后续对该volatile 字段的读操作。对volatile变量 v 的写入,与所有其他线程后续对 v 的读同步。
要满足这些条件,所以volatile关键字就有这些功能:
- 禁止缓存
- 对volatile变量相关的指令不做重排序
1.线程安全概念
1.1竞态条件与临界区
public class Demo{
public int i = 0;
public void incr(){
i++;
}
}
多个线程访问了相同的资源,向这些资源做了写操作时,对执行顺序有要求。
- 临界区:incr方法内部就是临界区域,关键的部分代码由多线程并发执行,会对执行结果产生影响。
- 竞态条件:可能发生在临界区域内的特殊条件。多线程执行incr方法时的i++关键代码,产生了竞态条件。
1.2共享资源
- 如果一段代码是线程安全的,则它不包含竞态条件。只有当多个线程更新共享资源时,才会发生竞态条件。
- 栈封闭时,不会在线程之间共享的变量,都是线程安全的。
- 局部变量引用本身不共享,但是引用的对象存储在共享堆中。如果方法内创建的对象,只是在方法中传递,并且不会对其他线程可用,那么也是线程安全的。
- 判定规则:如果创建、使用和处理资源,永远不会逃脱单个线程的控制,该资源的使用是线程安全的。
1.3不可变对象
public class Demo{
private int value = 0;
public Demo(int value){
this.value = value;
}
public int getValue(){
return this.value;
}
}
- 创建不可变的共享对象来保证对象在线程间共享时不被修改,从而实现线程安全。
- 实例被创建,value变量就不能再被修改,这就是不可变性。
2.原子操作定义
-
原子操作可以是一个步骤,也可以是多个步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分(不可中断性)。
-
将整个操作视为一个整体,资源在该次操作中保持一致,这是原子性的核心特征。
-
public class Demo{ public int i = 0; public void incr(){ i++; } }
-
上述代码中,i++操作分为三步骤执行,加载i、计算+1、赋值i。
3. CAS机制
3.1 CAS概念
- 比较和交换(Compare and swap)。属于硬件同步原语,处理器提供了基本内存操作的原子性保证。
- CAS操作需要输入两个数值,一个旧值A(期望操作前的值)和一个新值B,在操作期间先比较下旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。
- JAVA中sun.misc.Unsafe类,提供了compareAndSwapInt()和compareAndSwapLong()等几个方法实现CAS。
- 多线程并发调用CAS机制时,同一时间内只有一个线程能操作成功。这是操作系统硬件级别为我们提供的并发安全保证。
3.2 CAS的三个问题
- 循环+CAS,自旋的实现让所有线程都处于高频运行,争抢CPU执行时间的状态,如果操作长时间不成功,会带来很大的CPU资源消耗。
- 仅针对单个变量的操作,不能用于多个变量来实现原子操作。
- ABA问题(无法体现出数据的变动)
ABA问题的解决思路是这样的:
4.一种不能保证原子性的代码写法
/**
* 原子性, 两个线程,对i变量进行递增操作
*
* @author suvue
* @date 2020/2/10
*/
public class LockDemo {
volatile int i = 0;
public void add() {
i++;
}
public static void main(String[] args) throws InterruptedException {
LockDemo demo = new LockDemo();
for (int i = 0; i < 2; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
demo.add();
}
}
}).start();
}
Thread.sleep(2000L);
System.out.println(demo.i);
}
}
- 启动两个线程,分别对变量 i 进行自增1000次,输出结果,我们期望的结果应该为2000,但是正在的结果差强人意。可以复制代码运行。
5. Java中保证原子性的几种方式
5.1 Unsafe方式(不用加锁)
import sun.misc.Unsafe;
import java.lang.reflect.Field;
/**
* 原子性, 两个线程,对i变量进行递增操作
*
* @author suvue
* @date 2020/2/10
*/
public class LockDemo {
volatile int i = 0;
/**
* 直接操作内存,修改对象,数组内存....强大的API
*/
private static Unsafe unsafe;
/**
* 属性的偏移量
*/
private static Long iOffset;
static {
try {
// 反射技术获取unsafe值
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
//theUnsafe是静态成员变量,null就可以拿到
unsafe = (Unsafe) field.get(null);
//利用unsafe,通过属性的偏移量(定位到内存中对象内具体属性的内存地址)
iOffset = unsafe.objectFieldOffset(LockDemo.class.getDeclaredField("i"));
} catch (Exception e) {
e.printStackTrace();
}
}
public void add() {
// i++;// JAVA 层面三个步骤
// CAS + 循环 重试
int current;
do {
//CAS优化
current = unsafe.getIntVolatile(this, iOffset);
} while (!unsafe.compareAndSwapInt(this, iOffset, current, current + 1));
//可能会失败
}
public static void main(String[] args) throws InterruptedException {
LockDemo demo = new LockDemo();
for (int i = 0; i < 2; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
demo.add();
}
}
}).start();
}
Thread.sleep(2000L);
System.out.println(demo.i);
}
- 具体的步骤注释写在代码里了,可以对照看看。
5.2 JUC包下的AtomicInteger等类
下面贴出关键代码
private AtomicInteger i= new AtomicInteger(0);
public void add() {
i.incrementAndGet();
}
5.3 原子性实现方式小结
- 我们发现,同样是实现原子性,JUC包下的类使用起来很简便。
- 我们看下它是怎样实现的吧!
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
//...
- 上述是AtomicInteger的JDK源码,我们发现它居然也是用unsafe来实现的。与我们的第一种实现不谋而合!
6. JUC包内的原子操作封装类
类名 | 类描述 |
---|---|
AtomicBoolean | 原子更新布尔类型 |
AtomicInteger | 原子更新整型 |
AtomicLong | 原子更新长整型 |
AtomicIntegerArray | 原子更新整型数组里的元素 |
AtomicLongArray | 原子更新长整型数组里的元素 |
AtomicReferenceArray | 原子更新引用类型数组里的元素 |
AtomicIntegerFieldUpdater | 原子更新整型字段的更新器 |
AtomicLongFieldUpdater | 原子更新长整型字段的更新器 |
AtomicReferenceFieldUpdater | 原子更新引用类型里的字段 |
AtomicReference | 原子更新引用类型 |
AtomicStampedReference | 原子更新带有版本号的引用类型 |
AtomicMarkableReference | 原子更新带有标记位的引用类型 |
- Atomic类虽然可以保证原子性,但是在高并发下,非常损耗性能。因为同一时间只有一个线程能够修改成功,其它线程都处于循环尝试中,线程多的时候,对性能损耗非常大。
- 因此 jdk 1.8更新了新的原子操作封装类。
1.8更新
更新器:DoubleAccmulator、LongAccumulator
计数器:DoubleAdder、LongAdder
计数器增强版,高并发下性能更好
频繁更新但不太频繁读取的汇总统计信息时使用
分成多个操作单元,不同线程更新不同的单元
只有需要汇总的时候才计算所有单元的操作
- 每个线程去执行自增时,操作的是不同的变量,因此即使是高并发,也不存在线程争抢CPU的问题。
- 当获取计数器的值,会对所有的变量进行sum求和。
- 计算的时候很快,取结果的时候很慢,因为要进行累加操作。