java并发编程笔记(三)——线程安全性

时间:2023-01-08 13:23:14

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()方法的开始