线程安全性详解(原子性、可见性、有序性)()2

简介: 线程安全性详解(原子性、可见性、有序性)

3.1.4、AtomicReference


AtomicReference和AtomicInteger非常类似,不同之处就在于AtomicInteger是对整数的封装,底层采用的是compareAndSwapInt实现CAS,比较的是数值是否相等,而AtomicReference则对应普通的对象引用,底层使用的是compareAndSwapObject实现CAS,比较的是两个对象的地址是否相等。也就是它可以保证你在修改对象引用时的线程安全性。

多个线程之间的操作无论采用何种执行时序或交替方式,都要保证不变性条件不被破坏,要
保持状态的一致性,就需要在单个原子操作中更新相关的状态变量。
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicReference;
@Slf4j
public class AtomicReferenceExample {
    private static AtomicReference<Integer> count = new AtomicReference<>(0);
    public static void main(String[] args) {
        count.compareAndSet(0, 2); 
        count.compareAndSet(0, 1);
        count.compareAndSet(1, 3); 
        count.compareAndSet(2, 4); 
        count.compareAndSet(3, 5); 
        log.info("count:{}", count.get());
    }
}


大家觉得我们输出的结果会是多少?

返回结果:

15:26:59.680 [main] INFO com.mmall.concurrency.example.atomic.AtomicReferenceExample - count:4


为什么是4呢?

首先我们 要说的是public final boolean compareAndSet(V expect, V update)这个方法,这个方法主要的作用是通过比对两个对象,然后更新为新的对象,这里的比对两个对象,比对的方式不是equals而是==,意味着比对的是内存的中地址。


1、首先我们创建count的初始化为0

2、在main方法中 count.compareAndSet(0, 2);,判断count为0时赋值为2

3、在count.compareAndSet(0, 1);和 count.compareAndSet(1, 3);判断count是否为1或者0,因为上一步我们已经赋值为2了,所以判断不成立

4、在count.compareAndSet(2, 4);判断count是否为2,等式成立

5、最好输出结果为4


count.compareAndSet(0, 2); //count=0?赋值 2,判断成立,此时count=0,更新后为2
count.compareAndSet(0, 1); //count=0?赋值 1,判断不成立,此时count=2
count.compareAndSet(1, 3); //count=1?赋值 3,判断不成立,此时count=2
count.compareAndSet(2, 4); //count=2?赋值 4,判断成立,此时count=2,更新后count=4
count.compareAndSet(3, 5); //count=3?赋值 5,判断不成立,此时count=4


所以我们输出结果为:4


3.1.5、CAS中ABA问题的解决


CAS并非完美的,它会导致ABA问题,例如:当前内存的值一开始是A,被另外一个线程先改为B然后再改为A,那么当前线程访问的时候发现是A,则认为它没有被其他线程访问过。在某些场景下这样是存在错误风险的。比如在链表中。

如何解决这个ABA问题呢,大多数情况下乐观锁的实现都会通过引入一个版本号标记这个对象,每次修改版本号都会变话,比如使用时间戳作为版本号,这样就可以很好的解决ABA问题。

在JDK中提供了AtomicStampedReference类来解决这个问题,这个类维护了一个int类型的标记stamp,每次更新数据的时候顺带更新一下stamp。

3.2、原子性 — synchronized

synchronized是一种同步锁,通过锁实现原子操作。

1、修饰代码块:大括号括起来的代码,作用于调用的对象

2、修饰方法:整个方法,作用于调用的对象

3、修饰静态方法:整个静态方法,作用于所有对象

4、修饰类:括号括起来的部分,作用于所有对象

详细可以查看,我写的关于:synchronized的博客,因为写过所以就不做过多描述。

3.3、原子性 — 对比

  • Atomic:竞争激烈时能维持常态,比Lock性能好, 只能同步一个值
  • synchronized:不可中断锁,适合竞争不激烈,可读性好的情况
  • Lock:可中断锁,多样化同步,竞争激烈时能维持常态

四、线程安全性:可见性

简介:一个线程对主内存的修改可以及时被其他线程观察到

导致共享变量在线程间不可见的原因:

1.线程交叉执行

2.重新排序结合线程交叉执行

3.共享变量更新后的值没有在工作内存中与主内存间及时更新

4.1 可见性 — syncronized

JMM关于syncronized的两条规定:


线程解锁前,必须把共享变量的最新值刷新到主内存中

线程加锁时,将清空工作内存中共享变量的值,从而使得使用共享变量时需要从主内存中重新读取最新的值(注意:加锁与解锁是同一把锁)

由于syncronized可以保证原子性及可见性,变量只要被syncronized修饰,就可以放心的使用

4.2 可见性 — volatile

通过加入内存屏障禁止重排序优化来实现可见性。

具体实现过程:

对volatile变量写操作时,会在写操作后加入一条store屏障指令,将本地内存中的共享变量值刷新到主内存

对volatile变量读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量

volatile不能保证操作的原子性,也就是不能保证线程安全性, 如果需要使用volatile必须满足以下两个条件:


对变量的写操作不依赖与变量当前的值。

该变量没有包含在具有其他变量的不变的式子中。

所以volatile修饰的变量适合作为状态标记量。


注:以下图片为资料中获取,如有雷同,纯属巧合

屏幕快照 2022-05-10 下午1.37.40.png

示例:

import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
@Slf4j
public class VolatileExample {
    // 请求总数
    public static int clientTotal = 5000;
    // 同时并发执行的线程数
    public static int threadTotal = 200;
    public static volatile int count = 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);
    }
    private static void add() {
        count++;
    }
}

返回结果:


16:12:01.404 [main] INFO com.mmall.concurrency.example.count.VolatileExample4 - count:4986


通过执行代码我们可以发现,返回结果并不是我们想看到的5000,说明这个是线程不安全的类


主要是因为当我们执行conut++时分成了三步:

1、取出当前内存count值,这时count值时最新的

2、+1操作

3、重新写回主存


例如:有两个线程同时在执行count++,两个内存都执行了第一步,比如当前count值为99,它们都读到了这个count值,然后两个线程分别执行了+1,并写回主存,这样就丢掉了一次+1的操作。

五、线程安全性:有序性

  • 在JMM中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
  • 通过volatile、synchronized、lock保证有序性
  • 5.1 happens-before原则


程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作

锁定规则:一个unLock操作先行发生于后面对同一个锁的Lock()操作,也就是说只有先解锁才能对下面的线程进行加锁

volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作

传递规则:如果操作A先行发生与操作B,而操作B先行发生于操作C,则操作A先行发生于操作C

线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作,一个线程只有执行了start()方法后才能做其他的操作

线程终端规则:对线程interrupt()方法的调用先行发生与被中断线程的代码检测到中断事件的发生(只有执行了interrupt()方法才可以检测到中断事件的发生)

线程终结规则:线程中所有操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束,Thread.isAlive()的返回值手段检测到线程已经终止执行

对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

六、线程安全性:总结


原子性:Atomic包、CAS算法、synchronized、Lock

原子性做了互斥方法,同一个线程只能有一个进行操作

可见性:synchronized、volatile

一个主内存的线程如果进行了修改,可以及时被其他线程观察到,介绍了volatile如何被观察到的

有序性:happens-before原则

happens-before原则,观察结果,如果两个线程不能偶从happens-before原则观察出来,那么就不能观察他们的有序性,虚拟机可以随意的对他们进行重排序


目录
相关文章
|
25天前
|
缓存 安全 Java
多线程的三大特性:原子性、可见性和有序性
多线程的三大特性:原子性、可见性和有序性
23 0
|
4月前
|
缓存 算法 Java
多线程04 死锁,线程可见性
多线程04 死锁,线程可见性
23 0
|
4月前
|
Java
Java线程面试题:什么是原子性问题?如何解决?
Java线程面试题:什么是原子性问题?如何解决?
32 0
|
5月前
|
缓存 安全 Java
3.线程安全之可见性、有序性、原子性是什么?
3.线程安全之可见性、有序性、原子性是什么?
44 0
3.线程安全之可见性、有序性、原子性是什么?
|
5月前
|
Java
【Java】有 A、B、C 三个线程,如何保证三个线程同时执行?在并发情况下,如何保证三个线程依次执行?如何保证三个线程有序交错执行?
【Java】有 A、B、C 三个线程,如何保证三个线程同时执行?在并发情况下,如何保证三个线程依次执行?如何保证三个线程有序交错执行?
39 0
|
5月前
|
Java
【Java】有 A、B、C 三个线程,如何保证三个线程同时执行?在并发情况下,如何保证三个线程依次执行?如何保证三个线程有序交错执行?
> 在多线程的面试中,经常会遇到三个类似的线程执行问题: > Q1:有 A、B、C 三个线程,如何保证三个线程同时执行? > Q2:有 A、B、C 三个线程,在并发情况下,如何保证三个线程依次执行? > Q3:有 A、B、C 三个线程,如何保证三个线程有序交错执行? ## Q1:有 A、B、C 三个线程,如何保证三个线程同时执行? 保证线程同时执行可以用于并发测试。可以使用倒计时锁CountDownLatch实现让三个线程同时执行。代码如下所示: ```java ExecutorService executorService = Exec
73 0
|
10月前
|
缓存 安全 Java
【Java基础】线程的原子性、可见性、有序性及线程安全知识整理
一个操作或者多个操作,要么全部执行,并且执行的过程不会被打断, 要么就全部不执行(一个操作是不可被分割的)。
|
14天前
|
Java 数据库 Android开发
【专栏】Kotlin在Android开发中的多线程优化,包括线程池、协程的使用,任务分解、避免阻塞操作以及资源管理
【4月更文挑战第27天】本文探讨了Kotlin在Android开发中的多线程优化,包括线程池、协程的使用,任务分解、避免阻塞操作以及资源管理。通过案例分析展示了网络请求、图像处理和数据库操作的优化实践。同时,文章指出并发编程的挑战,如性能评估、调试及兼容性问题,并强调了多线程优化对提升应用性能的重要性。开发者应持续学习和探索新的优化策略,以适应移动应用市场的竞争需求。
|
2天前
|
Java 数据库
【Java多线程】对线程池的理解并模拟实现线程池
【Java多线程】对线程池的理解并模拟实现线程池
11 1
|
2天前
|
设计模式 消息中间件 安全
【Java多线程】关于多线程的一些案例 —— 单例模式中的饿汉模式和懒汉模式以及阻塞队列
【Java多线程】关于多线程的一些案例 —— 单例模式中的饿汉模式和懒汉模式以及阻塞队列
9 0