资讯专栏INFORMATION COLUMN

java高并发系列 - 第4天:JMM相关的一些概念

mengbo / 1133人阅读

摘要:我们需要先了解这些概念。在中,其表现在对于共享变量的某些操作,是不可分的,必须连续的完成。有序性有序性指的是程序按照代码的先后顺序执行。

JMM(java内存模型),由于并发程序要比串行程序复杂很多,其中一个重要原因是并发程序中数据访问一致性和安全性将会受到严重挑战。如何保证一个线程可以看到正确的数据呢?这个问题看起来很白痴。对于串行程序来说,根本就是小菜一碟,如果你读取一个变量,这个变量的值是1,那么你读取到的一定是1,就是这么简单的问题在并行程序中居然变得复杂起来。事实上,如果不加控制地任由线程胡乱并行,即使原本是1的数值,你也可能读到2。因此我们需要在深入了解并行机制的前提下,再定义一种规则,保证多个线程间可以有小弟,正确地协同工作。而JMM也就是为此而生的。

JMM关键技术点都是围绕着多线程的原子性、可见性、有序性来建立的。我们需要先了解这些概念。

原子性

原子性是指操作是不可分的,要么全部一起执行,要么不执行。在java中,其表现在对于共享变量的某些操作,是不可分的,必须连续的完成。比如a++,对于共享变量a的操作,实际上会执行3个步骤:

1.读取变量a的值,假如a=1
2.a的值+1,为2
3.将2值赋值给变量a,此时a的值应该为2

这三个操作中任意一个操作,a的值如果被其他线程篡改了,那么都会出现我们不希望出现的结果。所以必须保证这3个操作是原子性的,在操作a++的过程中,其他线程不会改变a的值,如果在上面的过程中出现其他线程修改了a的值,在满足原子性的原则下,上面的操作应该失败。

java中实现原子操作的方法大致有2种:锁机制、无锁CAS机制,后面的章节中会有介绍。

可见性

可见性是指一个线程对共享变量的修改,对于另一个线程来说是否是可以看到的。有些同学会说修改同一个变量,那肯定是可以看到的,难道线程眼盲了?

为什么会出现这种问题呢?

看一下java线程内存模型:

我们定义的所有变量都储存在主内存中
每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)
线程对共享变量所有的操作都必须在自己的工作内存中进行,不能直接从主内存中读写(不能越级)
不同线程之间也无法直接访问其他线程的工作内存中的变量,线程间变量值的传递需要通过主内存来进行。(同级不能相互访问)
线程需要修改一个共享变量X,需要先把X从主内存复制一份到线程的工作内存,在自己的工作内存中修改完毕之后,再从工作内存中回写到主内存。
如果线程对变量的操作没有刷写回主内存的话,仅仅改变了自己的工作内存的变量的副本,那么对于其他线程来说是不可见的。
而如果另一个变量没有读取主内存中的新的值,而是使用旧的值的话,同样的也可以列为不可见。

共享变量可见性的实现原理

线程A对共享变量的修改要被线程B及时看到的话,需要进过以下步骤:

1.线程A在自己的工作内存中修改变量之后,需要将变量的值刷新到主内存中
2.线程B要把主内存中变量的值更新到工作内存中

关于线程可见性的控制,可以使用volatile、synchronized、锁来实现,后面章节会有详细介绍。

有序性
有序性指的是程序按照代码的先后顺序执行。

为了性能优化,编译器和处理器会进行指令冲排序,有时候会改变程序语句的先后顺序,比如程序。

int a = 1; //1
int b = 20; //2
int c = a + b; //3
编译器优化后可能变成

int b = 20; //1
int a = 1; //2
int c = a + b; //3
上面这个例子中,编译器调整了语句的顺序,但是不影响程序的最终结果。

在单例模式的实现上有一种双重检验锁定的方式,代码如下:

public class Singleton {
  static Singleton instance;
  static Singleton getInstance(){
    if (instance == null) {
      synchronized(Singleton.class) {
        if (instance == null)
          instance = new Singleton();
        }
    }
    return instance;
  }
}

我们先看instance = new Singleton();

未被编译器优化的操作:

指令1:分配一款内存M
指令2:在内存M上初始化Singleton对象
指令3:将M的地址赋值给instance变量
编译器优化后的操作指令:

指令1:分配一块内存S
指令2:将M的地址赋值给instance变量
指令3:在内存M上初始化Singleton对象
现在有2个线程,刚好执行的代码被编译器优化过,过程如下:

最终线程B获取的instance是没有初始化的,此时去使用instance可能会产生一些意想不到的错误。

现在比较好的做法就是采用静态内部内的方式实现:

public class SingletonDemo {
    private SingletonDemo() {
    }
    private static class SingletonDemoHandler{
        private static SingletonDemo instance = new SingletonDemo();
    }
    public static SingletonDemo getInstance() {
        return SingletonDemoHandler.instance;
    }
}


喜欢就关注我吧

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/75936.html

相关文章

  • java并发系列 - 19:JUC中Executor框架详解1,全面掌握java并发相关技术

    摘要:有三种状态运行关闭终止。类类,提供了一系列工厂方法用于创建线程池,返回的线程池都实现了接口。线程池的大小一旦达到最大值就会保持不变,在提交新任务,任务将会进入等待队列中等待。此线程池支持定时以及周期性执行任务的需求。 这是java高并发系列第19篇文章。 本文主要内容 介绍Executor框架相关内容 介绍Executor 介绍ExecutorService 介绍线程池ThreadP...

    icattlecoder 评论0 收藏0
  • java并发系列 - 21javaCAS操作,java并发基石

    摘要:方法由两个参数,表示期望的值,表示要给设置的新值。操作包含三个操作数内存位置预期原值和新值。如果处的值尚未同时更改,则操作成功。中就使用了这样的操作。上面操作还有一点是将事务范围缩小了,也提升了系统并发处理的性能。 这是java高并发系列第21篇文章。 本文主要内容 从网站计数器实现中一步步引出CAS操作 介绍java中的CAS及CAS可能存在的问题 悲观锁和乐观锁的一些介绍及数据库...

    zorro 评论0 收藏0
  • 聊聊面试中关于并发问题应对方案

    摘要:这里呢,我直接给出高并发场景通常都会考虑的一些解决思路和手段结尾如何有效的准备面试中并发类问题,我已经给出我的理解。 showImg(https://segmentfault.com/img/bV7Viy?w=550&h=405); 主题 又到面试季了,从群里,看到许多同学分享了自己的面试题目,我也抽空在网上搜索了一些许多公司使用的面试题,目前校招和社招的面试题基本都集中在几个大方向上...

    xzavier 评论0 收藏0
  • Java 并发编程(学习)

    摘要:并发编程的挑战并发编程的目的是为了让程序运行的更快,但是,并不是启动更多的线程就能让程序最大限度的并发执行。的实现原理与应用在多线程并发编程中一直是元老级角色,很多人都会称呼它为重量级锁。 并发编程的挑战 并发编程的目的是为了让程序运行的更快,但是,并不是启动更多的线程就能让程序最大限度的并发执行。如果希望通过多线程执行任务让程序运行的更快,会面临非常多的挑战:(1)上下文切换(2)死...

    NervosNetwork 评论0 收藏0
  • 并发编程艺术

    摘要:假设不发生编译器重排和指令重排,线程修改了的值,但是修改以后,的值可能还没有写回到主存中,那么线程得到就是很自然的事了。同理,线程对于的赋值操作也可能没有及时刷新到主存中。线程的最后操作与线程发现线程已经结束同步。 很久没更新文章了,对隔三差五过来刷更新的读者说声抱歉。 关于 Java 并发也算是写了好几篇文章了,本文将介绍一些比较基础的内容,注意,阅读本文需要一定的并发基础。 本文的...

    curlyCheng 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<