线程安全性?
感谢 [原子性]https://blog.csdn.net/fanrenxiang/article/details/80623884
线程安全性主要体现在三个方面:原子性、可见性、有序性
- 原子性:提供了互斥访问,同一时刻只能有一个线程来对它进行操作
- 可见性:一个线程对主内存的修改可以及时的被其他线程观察到
- 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序。
本章主要关注一下原子性的方面
说到原子性,一共有两个方面需要学习一下,一个是JDK中已经提供好的Atomic包,他们均使用了CAS完成线程的原子性操作,另一个是使用锁的机制来处理线程之间的原子性。锁包括:synchronized、Lock
Atomic包中的类与CAS(compareAndSwap):
-
public final int getAndSet(int newValue) //给AtomicInteger设置newValue并返回加oldValue
-
public final boolean compareAndSet(int expect, int update) //如果输入的值和期望值相等就set并返回true/false
-
public final int getAndIncrement() //对AtomicInteger原子的加1并返回当前自增前的value
-
public final int getAndDecrement() //对AtomicInteger原子的减1并返回自减之前的的value
-
public final int getAndAdd(int delta) //对AtomicInteger原子的加上delta值并返加之前的value
-
public final int incrementAndGet() //对AtomicInteger原子的加1并返回加1后的值
-
public final int decrementAndGet() //对AtomicInteger原子的减1并返回减1后的值
采用的是incrementAndGet方法,此方法的源码中调用了一个名为unsafe.getAndAddInt的方法
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;
}
在此方法中,方法参数为要操作的对象Object var1、期望底层当前的数值为var2、要修改的数值var4。定义的var5为真正从底层取出来的值。采用do..while循环的方式去获取底层数值并与期望值进行比较,比较成功才将值进行修改。而这个比较再进行修改的方法就是compareAndSwapInt就是我们所说的CAS,它是一系列的接口,比如下面罗列的几个接口。使用native修饰,是底层的方法。CAS取的是compareAndSwap三个单词的首字母.
另外,示例代码中的count可以理解为JMM中的工作内存,而这里的底层数值即为主内存,如果看过我上一篇文章的盆友就能把这一块的知识点串联起来了。
AtomicLong 与 LongAdder
LongAdder在AtomicLong的基础上将单点的更新压力分散到各个节点,在低并发的时候通过对base的直接更新可以很好的保障和AtomicLong的性能基本保持一致,而在高并发的时候通过分散提高了性能。
缺点是LongAdder在统计的时候如果有并发更新,可能导致统计的数据有误差。
AtomicBoolean
这个类中值得一提的是它包含了一个名为compareAndSet的方法,这个方法可以做到的是控制一个boolean变量在一件事情执行之前为false,事情执行之后变为true。或者也可以理解为可以控制某一件事只让一个线程执行,并仅能执行一次。
AtomicIntegerFieldUpdater
这个类的核心作用是要更新一个指定的类的某一个字段的值。并且这个字段一定要用volatile修饰同时还不能是static的。
ABA问题
AtomicStampReference与CAS的ABA问题
ABA : 其实 比如 A想变成B,那么内存中的预期值就A,过去看还真是A,然后就交换;但是内存中的 A有可能是别的线程进行修改了的.A-->C-->D-->A.这种就和最初的设计思想不符.那么就加入了AtomicStampReference。
private static class Pair<T> { final T reference; final int stamp; private Pair(T reference, int stamp) { this.reference = reference; this.stamp = stamp; } static <T> Pair<T> of(T reference, int stamp) { return new Pair<T>(reference, stamp); } } private volatile Pair<V> pair; private boolean casPair(Pair<V> cmp, Pair<V> val) { return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val); } public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) { Pair<V> current = pair; return expectedReference == current.reference && expectedStamp == current.stamp && ((newReference == current.reference && //排除新的引用和新的版本号与底层的值相同的情况 newStamp == current.stamp) || casPair(current, Pair.of(newReference, newStamp))); }
AtomicStampReference的处理思想是,每次变量更新的时候,将变量的版本号+1,之前的ABA问题中,变量经过两次操作以后,变量的版本号就会由1变成3,也就是说只要线程对变量进行过操作,变量的版本号就会发生更改。从而解决了ABA问题。
解释一下上边的源码:
类中维护了一个volatile修饰的Pair类型变量current,Pair是一个私有的静态类,current可以理解为底层数值。
compareAndSet方法的参数部分分别为期望的引用、新的引用、期望的版本号、新的版本号。
return的逻辑为判断了期望的引用和版本号是否与底层的引用和版本号相符,并且排除了新的引用和新的版本号与底层的值相同的情况(即不需要修改)的情况(return代码部分3、4行)。条件成立,执行casPair方法,调用CAS操作。
AtomicLongArray
这个类实际上维护了一个Array数组,我们在对数值进行更新的时候,会多一个索引值让我们更新。
原子性,提供了互斥访问,同一时刻只能有一个线程来对它进行操作。那么在java里,保证同一时刻只有一个线程对它进行操作的,除了Atomic包之外,还有锁的机制。JDK提供锁主要分为两种:synchronized和Lock。接下来我们了解一下synchronized。
synchronized
依赖于JVM去实现锁,因此在这个关键字作用对象的作用范围内,都是同一时刻只能有一个线程对其进行操作的。
synchronized是java中的一个关键字,是一种同步锁。它可以修饰的对象主要有四种:
修饰代码块:大括号括起来的代码,作用于调用的对象
修饰方法:整个方法,作用于调用的对象
修饰静态方法:整个静态方法,作用于所有对象
修饰类:括号括起来的部分,作用于所有对象
具体的我就不写了 网上太多了
这里有个有趣的测试synchronized锁住的是代码还是对象
这个是很形象的写了 读后解读Synchronized下的三种锁:偏向锁 轻量锁 重量锁
Lock我需要整理 下次再说吧.
因为现在 synchronized现在加入了多种优化手段.其实效率来说 不低了.如果不能判断用Lock和synchronized 那就用synchronized吧
复制一份总结
原子性操作各方法间的对比
- synchronized:不可中断锁,适合竞争不激烈,可读性好 (其实我觉得现在效率可以了,可以直接用)
- Lock:可中断锁,多样化同步,竞争激烈时能维持常态
- Atomic:竞争激烈时能维持常态,比Lock性能好,每次只能同步一个值