跳转至

Concurrency

线程安全要保证 原子性 可见性 有序性

原子性保证指令在执行的过程中不会被打断,因为被打断的话会可能导致程序状态的改变,从而影响一致性

关于原子操作和锁的碎碎念

原子操作是天然互斥的 而利用原子操作通过控制逻辑和状态实现的锁形成的互斥区(线程不休眠的自旋锁 待线程休眠的锁 )可以使得互斥区内的语句都拥有互斥的特性*; 互斥归互斥 但是线程之间的顺序还是不确定的 同步包含了某种特殊的语义,规定了线程的某种顺序 如读写锁,条件变量等 形成了某种特殊语义的互斥 就是说操纵互斥操作,去实现特殊语义 如 消费者-生产者这种语义 同步其实就是消除多线程,把多线程限制成单线程或者执行结果上等价于单线程 因为程序在某些情况下,是要求同步的,所以在并发的情况下要保证某些语段的顺序性 互斥是实现同步的一种工具,互斥加逻辑控制***

关于 volatile 的碎碎念

 volatile是锁一个变量 volatile 不保证原子性,但是单单一个简单变量的读写操作是原子的
  • volatile 解决可见性问题 缓存一致(注意不是禁用缓存)和指令重排(编译器优化重排) 一写多读 且所以,volatile仅用在set或者get的场景,不适合用在getAndOperate场景以及set操作中修改变量前还有其他操作 只有简单的赋值操作是原子的 因为其不保证操作的原子性 一写多读没有读读竞争所以不需要原子性 不会出现两个i++导致 一致性被破坏

https://www.zhihu.com/question/329746124/answer/2140946313 volatile的好文 * 关于为什么要禁止指令重排,单线程环境里面编译器的指令重排会确保程序最终执行结果和代码顺序执行的结果一致,即考虑指令之间的数据依赖性。但是在多线程的情况下,编译器无法分析多线程下数据的依赖性,只会分析单个线程内的数据依赖性来进行指令的重排优化;这就破坏了程序原本的语义

       考虑一个使用双重检查锁定(Double-Checked Locking)的单例模式示例,其中的指令重排可能导致问题在上述代码中,getInstance 方法使用了双重检查锁定,意图在多线程环境下保证只创建一个单例对象。然而,这里存在指令重排的问题。*问题出现在实例化对象的那一行(instance = new Singleton();)。由于指令重排,可能会先执行对象的分配和构造,然后再将引用赋值给 instance,这可能导致其他线程在检查1处看到 instance 不为null,但实际上对象的构造可能还没有完成*为了解决这个问题,可以在 instance 变量上添加 volatile 关键字,以禁止指令重排

    ```java
    public class Singleton {

    private static Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {  // 检查1
            synchronized (Singleton.class) {
                if (instance == null) {  // 检查2
                    instance = new Singleton();  // 问题所在
                }
            }
        }
        return instance;
    }
    }
    ```

简单的说,修改volatile变量(而不是对简单变量的直接赋值,这是一个原子操作)分为四步:1)读取volatile变量到local;2)修改变量值;3)local值写回;4)插入内存屏障,即lock指令,让其他线程可见这样就很容易看出来,前三步都是不安全的,取值和写回之间,不能保证没有其他线程修改。原子性需要锁来保证。这也就是为什么,volatile只用来保证变量可见性,但不保证原子性。
***volatile实现的原理就是在加入一个内存屏障的指令
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;即指令重排不能跨过内存屏障;
2)它会强制将对缓存的修改操作立即写入主存; 
3)如果是写操作,它会导致其他CPU中对应的缓存行无效 
***

lock前缀指令(内存屏障),一个屏障会把这个屏障前写入的数据刷新到内存,这样任何试图读取该数据的线程将得到最新值,而不用考虑到底是被哪个cpu核心或者哪颗CPU执行的。如果你的字段是volatile,Java内存模型将在写操作后插入一个写屏障指令,在读操作前插入一个读屏障指令。

JMM(java内存模型)

为什么提到JMM?JMM当中规定了可见性、原子性、以及有序性的问题,在多线程中只要保证了以上问题的正确性,那么基本上不会发生多线程当中存在数据安全问题

JMM(Java Memory Model)本身是一种抽象的概念,并不真实存在,他描述的时一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。

JMM关于同步的规定: 线程解锁前,必须把共享变量的值刷新回主内存(即线程之间公共的内存 包括共用的缓存和主存) 刷出最新 线程加锁前,必须读取主内存的最新值到自己的工作内存(线程本地内存) 进来最新 加锁解锁时同一把锁

如果同一个变量i=0,有两个线程执行i++方法,线程1把i从内存中读取进缓存,而现在线程2也把i读取进缓存,两个线程执行完i++后,线程1写回内存,i = 1,线程2也写回内存i = 1,两次++结果最终值为1,这就是著名的缓存一致性问题。为了解决这个问题,前人给了两种方案

  • 总线锁 大锁 原子操作就是代表
  • 缓存一致性协议 volatile就是代表 cpu为了和各个硬件打交道方便,设计师们把每个硬件都连接一个线到cpu,但是发现这样太麻烦了,所以改为所有硬件都挂在总线上,cpu通过总线和各个硬件打交道。

如果使用总线锁,就阻塞了其他cpu和其他硬件交互(内存之类,磁盘,等等),i++这条语句就必须执行完了,其他cpu才能执行,否则只能一个cpu去和硬件交互。这也是一种解决办法,问题也明显,特别效率低下。

为了解决这个问题提出缓存一致性协议,具体协议就不讲,简单解释一下,如果我写入之后发现这是共享变量就使得其他cpu缓存了的值失效,让它再次去内存中读取

Atomic

Atomic类中的操作包含内存屏障(memory barriers)在Java中,对基本数据类型的变量进行简单的赋值操作通常是原子的。这是因为基本数据类型的赋值操作对应的是一条简单的字节码指令请注意,对于64位的长整型(long)和双精度浮点型(double)等数据类型,由于它们的操作可能涉及多条机器指令,因此在某些平台上,对它们的赋值操作可能不是原子的。在多线程环境下,这可能导致一些线程在读取这些变量时看到了不完整或不一致的值。

***atmic 和 ReentrantLock synchronized ReentrantLock 都会引入内存屏障 ***

信号量 锁 共享变量 无非都是对变量控制逻辑加对变量的原子操作结合来实现的

回到页面顶部