• 浅谈线程安全中的原子性,有序性和可见性(转载)


    对于Java并发编程,一般来说有以下的关注点:

    1. 线程安全性,正确性。

    2. 线程的活跃性(死锁,活锁)

    3. 性能

    其中线程的安全性问题是首要解决的问题,线程不安全,运行出来的结果和预期不一致,那就连基本要求都没达到了。

    保证线程的安全性问题,本质上就是保证线程同步,实际上就是线程之间的通信问题。我们知道,在操作系统中线程通信有以下几种方式:

    1. 信号量

    2. 信号

    3. 管道

    4. 共享内存

    5. 消息队列

    6. socket

    java中线程通信主要使用共享内存的方式。共享内存的通信方式首先要关注的就是可见性和有序性。而原子性操作一般都是必要的,所以主要关注这三个问题。

    1.原子性

    原子性是指操作是不可分的。其表现在于对于共享变量的某些操作,应该是不可分的,必须连续完成。例如a++,对于共享变量a的操作,实际上会执行三个步骤:

    1. 读取变量a的值

    2. a的值+1

    3. 将值赋予变量a 。

    这三个操作中任何一个操作过程中,a的值被人篡改,那么都会出现我们不希望出现的结果。所以我们必须保证这是原子性的。Java中的锁的机制解决了原子性的问题。

    2.可见性

    可见性是值一个线程对共享变量的修改,对于另一个线程来说是否是可以看到的。

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

    我们知道,java线程通信是通过共享内存的方式进行通信的,而我们又知道,为了加快执行的速度,线程一般是不会直接操作内存的,而是操作缓存。

    java线程内存模型:

                      

    实际上,线程操作的是自己的工作内存,而不会直接操作主内存。如果线程对变量的操作没有刷写会主内存的话,仅仅改变了自己的工作内存的变量的副本,那么对于其他线程来说是不可见的。而如果另一个变量没有读取主内存中的新的值,而是使用旧的值的话,同样的也可以列为不可见。

    对于jvm来说,主内存是所有线程共享的java堆,而工作内存中的共享变量的副本是从主内存拷贝过去的,是线程私有的局部变量,位于java栈中。

    那么我们怎么知道什么时候工作内存的变量会刷写到主内存当中呢?

    这就涉及到java的happens-before关系了。

    在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。

    这个人的博客写的不错:http://ifeve.com/easy-happens-before/。

    简单来说,只要满足了happens-before关系,那么他们就是可见的。

    例如:

    线程A中执行i=1,线程B中执行j=i。如果线程A的操作和线程B的操作满足happens-before关系,那么j就一定等于1,否则j的值就是不确定的。

    happens-before关系如下:

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

    2. 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作;

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

    4. 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;

    5. 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;

    6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;

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

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

    从上面的happens-before规则,显然,一般只需要使用volatile关键字,或者使用锁的机制,就能实现内存的可见性了。

    3.有序性

    有序性是指程序在执行的时候,程序的代码执行顺序和语句的顺序是一致的。

    为什么会出现不一致的情况呢?

    这是由于重排序的缘故。

    在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

    举个例子:

    线程A:

    context = loadContext();    
    inited = true;    

    线程B:

    while(!inited ){
     sleep
    }
    doSomethingwithconfig(context);

    如果线程A发生了重排序:

    inited = true;    
    context = loadContext(); 

    那么线程B就会拿到一个未初始化的content去配置,从而引起错误。

    因为这个重排序对于线程A来说是不会影响线程A的正确性的,而如果loadContext()方法被阻塞了,为了增加Cpu的利用率,这个重排序是可能的。

    如果要防止重排序,需要使用volatile关键字,volatile关键字可以保证变量的操作是不会被重排序的。

    作者:a60782885

    blog.csdn.net/a60782885/article/details/77803757

    冷眉横对千夫指,俯首甘为孺子牛。
  • 相关阅读:
    java 数据类型:集合接口Collection之List~ArrayList:remove移除;replaceAll改变原有值;sort排序;迭代器listIterator();
    java 数据类型:集合接口Collection之常用ArrayList;lambda表达式遍历;iterator遍历;forEachRemaining遍历;增强for遍历;removeIf批量操作集合元素(Predicate);
    java 常用类库:格式化NumberFormat;SimpleDataFormat类(转换Data()对象);DateTimeFormatter 转换LocalDateTime时间对象
    java 常用类库:时间类LocalDate;LocalTime;LocalDateTime;Calendar 类;Date ;
    java 常用类库:BigInteger大整数;BigDecimal大小数(解决double精度损失);
    java 常用类库:Math:常用min、max;floor;ceil;random;
    1345. Jump Game IV
    1298. Maximum Candies You Can Get from Boxes
    1293. Shortest Path in a Grid with Obstacles Elimination
    1263. Minimum Moves to Move a Box to Their Target Location
  • 原文地址:https://www.cnblogs.com/yujian0817/p/12753324.html
Copyright © 2020-2023  润新知