java并发编程笔记(三)——线程安全性
线程安全性:
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
线程安全体现在三个方面:
- 原子性:提供了互斥访问,同一时刻只能有一个线程来对它进行操作
- 可见性:一个线程对主内存的修改可以及时的被其他线程观察到
- 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序。
原子性:Atomic包
使用AtomicInteger保证该变量操作的原子性
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 Exception {
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(); //相当于++x;
// count.getAndIncrement(); //相当于x++
}
}
原理:AtomicInteger的incrementAndGet()方法里边用到了一个unsafe的类
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;
}
这里最重要的一个方法是:compareAndSwapInt(),这是java底层的一个方法,它不是通过java实现的:
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
参数解释:
Object var1:所操作的对象,比如本次案例中,这个Obect是AtomicInteger count;
long var2:这个对象当前的值;
int var4:当前对象要增加的值,比如本次案例中做+1操作,那么var4就是1;
int var5:调用底层得到的一个值,如果没有其他线程过来操作,这个值应该是等于var2
getAndAddInt()方法中compareAndSwapInt()方法执行解释:如果对于var1这个对象,如果var2与从底层获取的值var5是相同的,那么就执行var5 + var4;
进一步解释:count的当前值,是当前线程中的值,属于线程中的工作内存中的值,而底层获取的值是主存中值,只有当工作内存中的值和主存中的值是一致的时候,才可以修改。
AtomicLong、LongAdder
在上边的例子中,把AtomicInteger 替换成AtomicLong,整个方法依然是线程安全的。
第二种方式是使用LongAdder:
public class AtomicExample3 {
// 请求总数
public static int clientTotal = 5000;
// 同时并发执行的线程数
public static int threadTotal = 200;
public static LongAdder count = new LongAdder();
public static void main(String[] args) throws Exception {
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);
}
private static void add() {
count.increment();
}
}
AtomicLong和LongAdder的对比:
AtomicLong:该类底层实现是在一个死循环内,不断的尝试修改目标值,直到修改成功,在竞争不激烈的情况下,修改成功概率很大,在竞争激烈情况下修改失败的概率较大,这种情况下会有损性能。
LongAdder:由于Long、Double类型的值JVM允许将他们64位的读写操作分拆成32位的读写操作,根据此原理 LongAdder将操作的数值分拆成数组,然后最终得到的是数组的加和,通过分拆均衡操作压力,因此其性能相对较好
使用场景的选择:在高并发计数的情景下优先使用LongAdder,其他情况使用AtomicLong
**AtomicBoolean **
底层实现的方法是:
public final boolean compareAndSet(boolean expect, boolean update) {
int e = expect ? 1 : 0;
int u = update ? 1 : 0;
return unsafe.compareAndSwapInt(this, valueOffset, e, u);
}
这个方法是指某个代码块逻辑值执行一次。
使用案例(该案例演示了某一段代码在多线程情况下,只执行了一次):
public class AtomicExample6 {
private static AtomicBoolean isHappened = new AtomicBoolean(false);
// 请求总数
public static int clientTotal = 5000;
// 同时并发执行的线程数
public static int threadTotal = 200;
public static void main(String[] args) throws Exception {
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();
test();
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
log.info("isHappened:{}", isHappened.get());
}
private static void test() {
if (isHappened.compareAndSet(false, true)) {
log.info("execute");
}
}
}
AtomicReference、AtomicReferenceFieldUpdater
AtomicReference使用示例:
public class AtomicExample4 {
private static AtomicReference<Integer> count = new AtomicReference<>(0);
public static void main(String[] args) {
count.compareAndSet(0, 2); // 2
count.compareAndSet(0, 1); // no
count.compareAndSet(1, 3); // no
count.compareAndSet(2, 4); // 4
count.compareAndSet(3, 5); // no
log.info("count:{}", count.get()); //4
}
}
AtomicReferenceFieldUpdater使用示例:
public class AtomicExample5 {
private static AtomicIntegerFieldUpdater<AtomicExample5> updater =
AtomicIntegerFieldUpdater.newUpdater(AtomicExample5.class, "count");
@Getter
public volatile int count = 100;
public static void main(String[] args) {
AtomicExample5 example5 = new AtomicExample5();
if (updater.compareAndSet(example5, 100, 120)) {
log.info("update success 1, {}", example5.getCount());
}
if (updater.compareAndSet(example5, 100, 120)) {
log.info("update success 2, {}", example5.getCount());
} else {
log.info("update failed, {}", example5.getCount());
}
}
}
AtomicStampedReference:解决CAS的ABA问题
ABA问题:在CAS操作的时候,其他线程将变量的值A改成了B,但是又改回了A,本线程使用期望值A与当前变量进行比较的时候,发现变量A没有变,于是CAS将A值进行了交换操作。
解决思路:每次变量更新的时候,把版本号+1
核心类:
AtomicStampedReference
其中的核心方法:compareAndSet()
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)));
}
AtomicLongArray
这个类维护的是一个数组
这个类与AtomicLong比较,方法 里多了一个索引值让我们指定。
原子性——锁
- synchronized:依赖JVM去实现锁
- Lock:依赖特殊的cpu指令,代码实现,ReenteantLock
synchronized:
-
修饰代码块:大括号括起来的代码,作用于调用的对象
-
修饰方法:整个方法,作用于调用的对象
-
修饰静态方法:整个静态方法,作用于所有对象
-
修饰类,括号括起来的部分,作用于所有对象
原子性——对比
synchronized:不可中断锁,适合竞争不激烈,可读性好
Lock:可中断锁,多样化同步,竞争激烈时能维持常态
Atomic:竞争激烈时能维持常态,比Lock性能好;只能同步一个值
可见性
导致共享变量在线程间不可见的原因
- 线程交叉执行
- 重排序结合线程交叉执行
- 共享变量更新后的值没有在工作内存与主内存间及时更新
可见性——synchronized
JMM关于synchronized的两条规定:
- 线程解锁前,必须把共享变量的最新值刷新到主内存
- 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意:加锁和解锁是同一把锁)
可见性——volatile
通过假如内存屏障和禁止重排序优化来实现
- 对volatile变量写操作时,会在写操作后加入一条store屏障指令,将本地内存中的共享变量值刷新到主内
- 对volatileb变量读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量
volatile关键字不具有原子性
适合的场景:
- 对变量的写操作不依赖与当前值;
- 该变量没有包含在具有其他变量的不变式中。
因此volatile特别适合状态标记量
有序性
java内存模型中,允许编辑器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
通常情况下可以通过以下三个关键字来保证有序性:
- volatile
- synchronized
- Lock
happens-before原则
如果两个操作的执行次序无法从happens-before原则推导出来,那么就不能保证他们的有序性,虚拟机就可以对他们随意的进行重排序。
也就是除了下面这些规则规定的场景,其他场景,虚拟机可以对其进行重排序。
-
程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
-
锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作
-
volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
-
传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
-
线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作
-
线程中断原则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
-
线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行。
-
对象终结规则:一个对象的初始化完成先行发生于它的finalize()方法的开始