对象的共享

【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>> hot3.png

一、可见性

      对象的可见性指的是在多个线程共享变量时,一个线程对变量的修改是否对另一个线程是否是可见的。因为在Java内存模型中,每个线程都有自己独有的内存空间,并且对于共享变量,每个线程都会对其进行缓存到自己的独有空间中。因而当一个线程对其缓存空间内的对象值进行修改时,其不一定会立即更新到其他线程中。对象的可见性有几个比较重要的概念:

  1. 失效数据:失效数据指的是一个线程对共享变量进行了修改,而其值没有立即更新到另一个线程的共享空间中,因而另一个对象看到的就是该变量修改之前的值,这个值就是失效数据。
  2. 重排序:假如有多个线程操作多条数据,其中有A和B两个数据,并且某个线程先对A进行了修改,然后对B进行了修改,此时另外的线程获取A和B这两个数据的更新值的顺序并不一定是由更新的顺序(由A到B)来获取A和B的最新值的,其更新顺序也可能相反,这就是“重排序”。
  3. 非原子的64位操作:在Java的内存模型中,变量的读取和写入操作都必须是原子的,但是对于long和double这样的64位数据,JVM允许对这两种类型的变量的读取和写入操作分成两个32位的部分依次进行,这样就导致可能两个线程,一个线程正在读取共享变量的高位部分,而另一个线程则正在更新该变量的高位部分,从而导致该变量的读取和更新操作不是原子的。

      这里要对同步代码块中要求使用同一个对象的内置锁的原因进行说明。因为内置锁是互斥的,因而同一时刻只有一个线程能够获取到该内置锁,这就保证了使用内置锁保护的代码块同一时刻只能有一个线程执行;另一方面,同一个内置锁能够记录其保护的代码块中的变量更新前后的最新值,这也就保证了多个线程在执行该代码块的时候能够始终读取到共享变量的最新值,也就保证了可见性。

      Java提供了一种较弱的方式来保证变量的可见性:volatile关键字。当把一个变量使用volatile声明时,编译器和运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序,并且也不会将其缓存在寄存器或其他处理器不可见的地方,因而读取和写入volatile变量时都是获取的最新值。

      这里需要说明的是,加锁机制既可以确保可见性,也可以确保原子性,而volatile变量只能确保原子性。当且仅当满足以下条件时,才应该使用volatile变量:

  1. 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值;
  2. 该变量不会与其他状态变量一起纳入不变性条件中;
  3. 在访问变量时不需要加锁;

二、发布和逸出

      发布:发布的意思是指,使对象能够在当前作用域之外的代码中使用;

      逸出:当某个不应该发布的对象被发布时,这种情况称为逸出;

      当发布一个对象时,可能会简介的发布其他对象:①当发布一个对象时,在该对象的非私有域中引用的所有对象都会被发布;②如果一个已经发布的对象能够通过非私有的引用和方法而达到其他的对象,这样也会发布对象;③当发布一个对象内部类的实例时,也隐含的将该对象进行了发布。

      这里需要说明的是,当在构造函数中启用线程时可能会将this引用发布出去,如果确实要在构造函数中创建线程,那么也不要立即启动这个线程(因为构造过程中this引用逸出可能会造成另外的线程在其还未构造完全时对其状态进行了读取或改变)。可改写的一种方式是使用工厂方法,在工厂方法中首先创建该对象实例,然后通过该实例进行需要在构造函数中执行的方法。

三、线程封闭

      线程封闭指的是解除对共享变量的操作,而将其转换为在每个线程中进行,这就是线程封闭。由于在线程中的变量都是独属于该线程的,因而对这些变量的操作都是线程安全的。线程封闭主要有三种类型:

  1. Ad-hoc线程封闭:Ad-hoc线程封闭是指将维护线程封闭性的职责完全由程序实现来承担。该封闭原则主要是通过框架来将变量封装到指定线程上,而现在没有任何一种语言特性能确保将对象封装到目标线程上。
  2. 栈封闭:栈封闭指的是将共享变量转换为局部变量,因为局部变量的固有属性之一就是将其封闭在执行线程中,因而栈封闭是线程安全的。
  3. ThreadLocal类:ThreadLocal类能够使线程中某个值与保存值的对象关联起来,并且为该变量创建一个独立的副本,因此get方法总是返回由当前前程在调用set方法时设置的最新值。当频繁执行的操作需要一个临时对象,并且希望避免每次执行时都重新分配该临时变量,就可以使用该技术。ThreadLocal<T>可以理解为包含了Map<Thread, T>的对象,其中保存了特定于该线程的值。

四、不变性

      不变性对象指的是该对象从创建之后其状态就不能发生改变的对象。不变性对象由于无法对其进行更新,因而其一定是线程安全的。一个对象是否为不可变对象需要满足以下几个条件:

  1. 对象创建之后其状态就不可改变;
  2. 对象的所有域都是final类型;
  3. 对象是正确创建的(对象创建期间,this引用没有逸出);

      这里有需要说明的是,①final关键字能够保证其修饰的对象能够拥有安全的初始化过程,因而多个线程可以不受限制的访问final域;②如果一段程序有多个可变的状态,那么可以将这几个状态抽离到一个不可变类中,在该类中对可变状态的设置放在构造函数中,并且每次获取可变状态时都返回其拷贝,另外需要对该类的实例使用volatile进行修饰,以确保其可见性。

五、安全发布

      不可变对象:任何线程都可以在不需要额外同步的情况下安全地访问不可变对象,即使在发布这些对象时没有使用同步。

      安全发布:要安全的发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式来安全地发布:

  1. 在静态初始化函数中初始化一个对象的引用;
  2. 将对象的引用保存到volatile类型的域或者AtomicReference对象中;
  3. 将对象的引用保存到某个正确构造对象的final类型域中;
  4. 将对象的引用保存到一个由锁保护的域中;

      线程安全容器的使用:

  1. 通过将一个键或者值放入Hashtable、synchronizedMap或者ConcurrentMap中,可以安全地将它发布给任何从这些容器中访问它的线程(无论是直接访问还是通过迭代器访问);
  2. 通过将某个元素放入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList或synchronizedSet中,可以将该元素安全的发布到任何从这些容器中访问该元素的线程;
  3. 通过将某个元素放入BlockingQueue或者ConcurrentLinkedQueue中,可以将该元素安全地发布到任何从这些队列中访问该元素的线程。

      事实不可变对象:如果对象在发布后其状态不会被修改,那么该对象就是事实不可变对象。对于事实不可变对象,只要对其进行安全的发布,并且其状态对于所有线程都可见时,那么就可以安全地发布该对象。

      可变对象:如果对象在构造之后可以修改,那么就必须确保其能够安全发布,并且发布之后需要对其所有的操作进行同步,这样才能够安全的访问该对象。

      对象的访问方式有如下几种,每一种都有其都有的多线程安全性:

  1. 线程封闭:线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改;
  2. 只读共享:在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象;
  3. 线程安全共享:线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步;
  4. 保护对象:被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值