对象的共享

对于可变状态的管理

同步-线程安全性

共享和发布变量

  • 同步对象的状态

可见性

什么是可见性?

  • 对于状态的修改,对于其他线程是能够及时同步到的,称之为可见性

重排序

  • 为什么需要重排序?

    • 如果代码严格按照我们所编写的代码顺序执行,则会出现如下问题

      • 假设有三个操作,代码顺序如下 A写入流 B执行其他操作 C写入流,如果此时按照正常指令顺序执行,则AC会写入流及关闭流两次,而如果把指令顺序重排序为ACB,则只需要写入流两次,关闭流一次,优化了程序性能
  • 概念

    • 编译器重排序

    • 处理器重排序

    • 数据依赖性(单处理器)

      • 三个场景

        • 写后读
        • 写后写
        • 读后写
      • 上述三个场景包含数据依赖,因此不会进行编译器和处理器的重排序

      • 不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑

    • as-if-serial语义

      • 不管怎么重排序,(单线程)程序的执行结果不能被改变
      • 编译器和处理器不会对存在数据依赖关系的操作做重排序(单线程!!!!),相当于将单线程程序保护了起来
    • happens-before关系

      • 概念

        • 如果一个操作执行的结果需要对另外一个操作可见,那么这两个操作之间就要存在happens-before关系且第一个操作的执行顺序于第二个操作之前!(不然会存在并发问题)

          • jmm的承诺
        • 如果两个操作之间存在happens-before的关系但是并不代表java对两个操作的执行顺序需要按照h-b指定的顺序来执行,只要执行的结果与h-b顺序执行的结果一致。那么这种重排序就是允许的(单线程or多线程)

      • 规则

        • 程序顺序规则:一个线程中的每个操作happens-before 该线程中的任意后续操作
        • 监视器锁规则:对一个锁的解锁 happens-before 随后对这个锁的加锁。
        • volatile变量规则:对一个volatile域的写 happens-before 任意后续对这个volatile域的读。
        • 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
        • start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作 happens-before 线程B中的任意操作。
        • join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
  • 引发的并发问题--导致数据出现可见性的问题

    虽然重排序不会影响单个线程内程序执行的结果,但是多线程呢?下面看一个例子:
    //线程1:
    context = loadContext();   //语句1
    inited = true;             //语句2

    //线程2:
    while(!inited ){
      sleep()
    }
    doSomethingwithconfig(context);
     上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。
     从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。

    • 指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。

最低安全性(失效性)

  • 线程读取的一个变量可能为过时的数据,这种安全性叫做最低安全性(适用于绝大多数变量,非volatile类型修饰的double、long除外)

    • 由于运行对long和double分解为两个32位的操作,因此多线程场景下上述类型不是线程安全的
  • 所以针对所有的可变的共享变量需要用同一个锁保护,这样可保证所以线程对该共享变量的修改对其他线程是可见的

解决可见性的问题

  • volatile变量

    • 概念

      • 在多线程环境下保住了变量的可见性
    • 访问时不会加锁

    • 如何确保可见性?

      两个特点的实现原理!
      在 volatile 变量的赋值操作的反编译代码中,在执行了赋值操作之后加了一行:lock addl $0x0,(%esp)
      这一句的意思是:给 ESP 寄存器 +0,是一个空操作,重点在 lock 上
      首先 lock 的存在相当于一个内存屏障,使得重排序时,不能把后面的指令排在内存屏障之前
      同时,lock 指令会将当前 CPU 的 Cache 写入内存,并无效化其他 CPU 的 Cache,相当于对 Cache 中的变量做了一次 store -> write 操作
      这使得其他 CPU 可以立即看见 volatile 变量的修改,因为其他 CPU 在读取 volatile 变量前会先从主内存中读取 volatile 变量,即进行一次 read -> load 操作

    • 应用场景

      • 某个操作完成的标志
      • 发生中断的标志
    • 不适用的场景

      • 多线程访问下,对变量的写入依赖当前值,例如自增

        • 因为volatile关键字无法保证原子性

          简单的说,修改volatile变量分为四步:
          1)读取volatile变量到local
          2)修改变量值
          3)local值写回
          4)插入内存屏障,即lock指令,让其他线程可见
          这样就很容易看出来,前三步都是不安全的,取值和写回之间,不能保证没有其他线程修改。原子性需要锁来保证。
          这也就是为什么,volatile只用来保证变量可见性,但不保证原子性

      • 该变量不会和其他变量一起纳入正确性的条件

  • 加锁

    • 加锁机制既可以确保原子性也可以确保可见性

发布与溢出

发布

  • 概念

    • 将引用暴露出去给别的地方使用

溢出

  • 概念

    • 发布了不该发布的对象由此导致的安全性

如何确保状态不被溢出来保证安全性?

  • 不要在构造方法中溢出当前未构造完成的对象的this引用

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×