多线程,Java内存模型解决线程安全问题 jvm

操作同一共享变量时,存在线程安全问题,JMM java内存模型,当多线程操作同一共享变量,先进行主线程的变量加载到本地线程一个副本,然后回写到主线程。这样就会存在,多个线程加载变量相同,非可见性。
java并发编程三大特性:原子性 可见性 有序性
volatile 解决 可见性 有序性
线程对共享变量的副本做了修改,会立刻刷新最新值到主内存中。
线程对共享变量的副本做了修改,其他其他线程中对这个变量拷贝的副本会时效;其他线程如果需要对这个共享变量进行读写,必须重新从主内存中加载。

当对volatile变量进行写操作的时候,JVM会向处理器发送一条lock前缀的指令,将这个缓存中的变量回写到系统主存中。
通过缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。

synchronized解决原子性也可以解决可见性。 jdk1.5推出的lock也可以解决这个问题。lock性能有提升。但是需要稍微多的操作。
syn 原理,monitor监视器,监控对象一个锁。monitor入口和出口进行锁的判断和控制。
synchronized:使用synchronized代码块或者synchronized方法也可以保证共享变量的可见性。当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。当线程获取锁时,JMM会把该线程对应的本地内存置为无效,从而使得被监听器保护的临界区代码必须从主内存中读取共享变量,从而实现共享变量的可见性。

synchronized:使用synchronized代码块或者synchronized方法也可以保证共享变量的可见性。当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。当线程获取锁时,JMM会把该线程对应的本地内存置为无效,从而使得被监听器保护的临界区代码必须从主内存中读取共享变量,从而实现共享变量的可见性

Volatile通过内存屏障来实现可见性的 sync*是怎么实现可见的?

wait notify
必须有锁 才进行 等待 唤醒。
线程对象的等待和唤醒,可以解决,一些编程问题,比如,按顺序输出奇偶数,生产者消费者问题。

• 并发问题产生的三大根源是「可见性」「有序性」「原子性」

• 可见性:CPU架构下存在高速缓存,每个核心下的L1/L2高速缓存不共享(不可见)

• 有序性:主要有三部分可能导致打破(编译器和处理器可以在不改变「单线程」程序语义的情况下,可以对代码语句顺序进行调整重新排序

  • ◾编译器优化导致重排序(编译器重排)
  • ◾指令集并行重排序(CPU原生重排)
  • ◾内存系统重排序(CPU架构下很可能有store buffer /invalid queue 缓冲区,这种「异步」很可能会导致指令重排)
    • 原子性:Java的一条语句往往需要多条 CPU 指令完成(i++),由于操作系统的线程切换很可能导致 i++ 操作未完成,其他线程“中途”操作了共享变量 i ,导致最终结果并非我们所期待的。
    • 在CPU层级下,为了解决「缓存一致性」问题,有相关的“锁”来保证,比如“总线锁”和“缓存锁”。

◾ 总线锁是锁总线,对共享变量的修改在相同的时刻只允许一个CPU操作。

◾ 缓存锁是锁缓存行(cache line),其中比较出名的是MESI协议,对缓存行标记状态,通过“同步通知”的方式,来实现(缓存行)数据的可见性和有序性
◾ 但“同步通知”会影响性能,所以会有内存缓冲区(store buffer/invalid queue)来实现「异步」进而提高CPU的工作效率
◾ 引入了内存缓冲区后,又会存在「可见性」和「有序性」的问题,平日大多数情况下是可以享受「异步」带来的好处的,但少数情况下,需要强「可见性」和「有序性」,只能"禁用"缓存的优化。
◾ “禁用”缓存优化在CPU层面下有「内存屏障」,读屏障/写屏障/全能屏障,本质上是插入一条"屏障指令",使得缓冲区(store buffer/invalid queue)在屏障指令之前的操作均已被处理,进而达到 读写 在CPU层面上是可见和有序的。

• 不同的CPU实现的架构不一样,Java为了屏蔽硬件和操作系统访问内存的各种差异,提出了「Java内存模型」的规范,保证了Java程序在各种平台下对内存的访问都能得到一致效果。

有趣的实例:

// 运行此类,验证多线程场景,主内存变量 本地线程变量不可见问题
// 但是如果使用System.out.println时,程序主动退出,发现其println用到了锁影响的。
public class SynTest {
    private static boolean bool = true;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (bool) {
                // System.out.println("11111111");
                // synchronized (new Object()) {}
            }
            System.out.println("Thread0 - 结束");
        });
        t1.start();
        Thread.sleep(1000);
        bool = false;
        System.out.println("main - 结束");
    }
}

通过查看println源码,可以发现println语句中有一个上锁的操作:

在使用了synchronized上锁这个操作后线程会做以下操作:

  1.获得同步锁
  2.清空工作内存
  3.从主内存中拷贝对象副本到本地内存
  4.执行代码(打印语句或加加操作)
  5.刷新主内存数据
  6.释放同步锁

这也就是System.out.println()为何会影响内存可见性的原因了。
因此总结,当前线程中加synchronized锁,就能实现可见性。当且仅当影响当前线程


参见:
https://mp.weixin.qq.com/s/z69rzL_LvxRh5K96-F2Y4w
https://mp.weixin.qq.com/s/j2I5YQ0qdmeFZVvpuFSyvg
https://mp.weixin.qq.com/s/VvF6tVjHbLtxQGtofhD5wQ

posted @ 2021-04-25 09:29  倔强的老铁  阅读(138)  评论(0编辑  收藏  举报