java 对象共享_Java 并发编程_对象共享

要想写出正确的并发程序,我们首要考虑的问题是:在访问共享的可变状态时,需要进行正确的管理。

✨ 可见性

可见性,词如其名。这个词一般是形容线程与线程之间修改同一资源的时候,能不能看得见对方所修改的内容。

可能理解起来有点绕。举个例子

public class NoVisiablity{

private static boolean ready;

private static int number;

private static class RenderThread extends Thread{

public void run() {

while(!ready) {

Thread.yield();

}

System.out.println(number);

}

}

public static void main(String[] args) {

new RenderThread().start();

number = 42;

ready = true;

}

}

理想情况来说的话,我是希望它输出 42。但是理想有时候并不能实现。有可能它会输出 0。因为可能会有”重排序“的情况。

所谓的重排序,是计算机处理器提升处理能力的方法之一。让我们来看看其定义

在没有同步的情况下,编译器/处理器以及运行时等可能对操作的执行顺序进行一些意想不到的调整。在缺乏足够同步的多线程中,要想对

内存操作的执行顺序进行判断,几乎无法得出正确的结论。

这个设计有点反人类(因为我一直认为计算机就应该按照我的代码顺序执行才是可靠的),但是它却能提高极高的性能。JVM 不仅自身的编译器

上面允许对操作顺序进行重排序(数值缓存再寄存器),而且允许 CPU 对操作顺序重排序(将数值缓存在特定的缓存中)。

✨✨ 失效数据

失效数据是指,当线程查看 ready 变量时,有可能得到一个失效的值。除非在每次访问变量都使用同步,否则很可能

获得该变量是一个失效值。

失效数据可能导致的问题是,循环无法结束,意料之外的异常,不精确的计算以及被破坏的数据结构。

而避免失效值的方法,是使操作的对象成为一个安全同步的对象,例如对其进行同步。

✨✨ 非原子的 64 操作

当线程在没有同步的情况下,可能会得到一个失效值,但至少这个值是由之前某个线程的值,而不是一个随机值。这种安全性保证也被称为最低安全性。

最低安全性适用于绝大多变量,但存在一个例外,非 volatile 类型的 64 位数变量(double 和 long)。

Java 内存模型要求,变量的读取操作和写入操作都必须是原子操作,但对于非 volatile 类型的 64 位数位变量, JVM 允许将 64 位

的读操作或写操作分解为两个 32 位的操作。当读一个非 volatile 类型的 long 变量时,如果对该变量的读操作和写操作在不同的线程中执行,那么很可能会读取到某个值的高 32 位和另外一个值的 32 位。因此,即使不考虑数据失效问题,在很多线程程序中使用共享且可变的 long 和 double 等类型的变量也不安全,除非使用 volatile 或 锁。

✨✨ 加锁与可见性

加锁的含义不仅仅局限于互斥行为,还有内存可见性。为了确保所有的线程都能看到共享变量的最新值,所有执行读操作或写操作的线程都必须在同一个锁上同步。

✨✨ Volatile 变量

Java 提供了一种稍弱的同步机制,即 volatile 变量,用于确保将变量的更新操作同志到其他线程。原理就是,当变量被声明为 volatile,编译器与运行时都会注意这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起进行重排序。也就是 volatile 变量不会被缓存在寄存器或对其他处理器不可见,因此读取 volatile 类型变量时总会返回最新写入的值。

仅当 volatile。变量能简化代码的实现以及对同步策略的验证时,才应该使用它们。如果在验证正确性需要对可见性进行复杂的判断,那么就不要使用 volatile 变量。volatile 变量的正确使用方式:确保它们自身状态的可见性,确保它们所引用对象的状态的可见性,以及标识一些重要的程序生命周期事件的发生(例如,初始化或关闭)。

✨ 发布与逸出

“发布”一个对象是指,使对象能够在当前作用域之外的代码中使用。例如说,将对象的引用保存到别的代码的地方,或者一个非私有的方法中返回该引用,或者将引用传递到其他类的方法中。

而“逸出”是指一个不应该被发布的对象被发布了。

class UnsafeStates {

private String[] states = new String[] {

"AK", "AL" ...

}

public String[] getState() { return states; }

}

因为 states 是私有变量,但是却通过非私有方法发布了该变量,导致了该变量逸出了它的作用域,这样子值就很有可能被随便改变,你就无法预测哪些代码会执行改变,也不知道在外部方法中究竟会发布这个对象,还是会保留对象的引用并随后由另外一个线程执行。这一些事会让你失去对结果的把控。当某个对象逸出后,你必须假设有某个类或线程可能会误用该对象。这正是需要使用封住的最主要原因:封装能够使得对程序的正确性进行分析变得可能,并使得无意中破坏了设计约束条件变得更难。

为了避免错误的发布对象,我们有几种方式解决:

将对象引用保存到一个公有的静态变量中,以便于任何类和线程都可见。

✨✨ 安全的对象构造过程

✨ 线程封闭

常常我们是因为需要共享可变数据时,通常需要使用同步。一种避免使用同步的方式就是不共享数据。如果在单线程内访问数据,就不需要同步。这种技术被称为

线程封闭,它是实现线程安全性的最简单方式之一。当某个对象封装在一个线程时,这种用法将会自动实现线程安全性,即使被封闭的对象本身不是线程安全的。

✨✨ Ad-hoc 线程封闭

✨✨ 栈封闭

栈封闭是线程封闭的一种特例。在栈封闭中,只有通过局部变量才能访问对象。就好像封装可以使得代码更加容器维持不变性条件那样,同步变量也能使对象更易于封闭在线程中。

✨✨ThreadLocal 类

ThreadLocal 是维持线程封闭性的一种更规范的方法。

private static ThreadLocal connectionHoder

= new ThreadLocal() {

return DriverManager.getConnection(DB_URL);

}

public static Connection getConnection() {

return connectionHoder.get();

}

✨ 不变性

满足同步需求的另一种方法是使用不可变对象。我们在解决多线程时的相关问题,例如失效数据/丢失更新操作/观察到某个对象不一致的状态等等,

其实根本原因是因为多线程试图更改访问一个可变的状态。那么如果一个状态不可变,那么这些并发带来的问题与复杂性也消失了。

不可变的对象一定是线程安全的

那什么是不可变对象呢?其实并不是仅仅使用 final 修饰即可。即使对象中所有的域都声明为 final 类型,即使对象中所有的域都是 final 类型,这个对象仍然是可变的,

因为在 final 类型的域中可以保存对可变对象的引用。

所以当满足以下条件时,对象才是不可变的

对象创建以后其状态就不能修改了。

对象的所有域都是 final 类型。

对象是正确创建的(在对象的创建期间,this 引用没有逸出)。

给出一个正确的在可变对象基础上构建的不可变类

public final class ThreeStoage {

private final Set stoages = new HashSet();

public THreadStoages() {

stoages.add("Moe");

stoages.add("Larry");

stoages.add("Curly");

}

public boolean isStoage(String name) {

return stoages.contains(name);

}

}

在构造器阶段已经完成了初始化,之后将无法对其进行修改。

✨✨ Final 域

final 域用于构造不可变性对象。说明了 final 类型的域是不能修改的。(如果 final 域所引用的对象是可变的,那么这些被引用的对象是可以修改的。)

然而,在 Java 内存模型中,final 域还有特殊的语义。final 域可以确保初始化过程的安全性,从而可以不受限制地访问不可变对象,并共享这些对象时无须同步。

✨✨ 使用 volatile 类型来发布不可变对象

class OneValueCache {

private final BigInteger lastNumber;

private final BigInteger[] lastFactors;

public OneValueCache(BigInteger i, BigInteger[] lastFactors) {

lastNumber = i;

lastFactors = Arrays.copyOf(factors, factors.length);

}

public BigInteger[] getFactors(BigInteger i) {

if(lastNumber == null || lastNumber.equal(i)) {

return null;

}

else {

return Arrays.copyOf(lastFactors, lastFactors.length);

}

}

}

✨ 安全发布

// 不安全的发布

public Holder holder;

public void initialize() {

holder = new Holder(42);

}

✨✨ 不正确的发布:正确的对象被破坏

✨✨ 不可变对象与初始化安全性

✨✨ 安全发布的常用模式

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

在静态初始化函数中初始化一个对象引用。

将对象的引用保存到 volatile 类型的域或 AtomicReference 对象中。

将对象的引用保存到某个正确构造对象的 final 类型域中。

将对象的引用保存到一个由锁保护的域中。

✨✨ 事实不可变对象

在没有额外同步的情况下,任何线程都可以安全地使用被安全发布的事实不可变对象。

✨✨ 可变对象

如果对象在构造时可以修改,那么安全发布只能确保“发布当时”的可见性,对于可变对象,不仅在发布时需要使用同步,而且在每次对象

访问时同样需要使用同步来确保后续修改操作的可见性。要安全地共享可变对象,这些对象就必须被安全的发布,并且必须是线程安全的或者

由某个锁保护起来。

对象的发布需求取决于它的可变性:

不可变对象可以通过任意机制来发布

事实不可变对象必须通过安全方式来发布

可变对象必须通过安全方式来发布,并且必须是线程安全的或由某个锁保护起来。

✨✨ 安全地共享对象

在并发程序中使用和共享对象时,可以使用一些实用的策略,例如

线程封闭。线程封闭的对象只由一个线程拥有,对象被封闭在线程中,并且只能由这个线程修改。

只读共享。在没有额外的同步情况下,共享的只读对象可以由多个线程访问,但任何线程不可修改它。共享的只读对象包括不可变对象和事实不可变对象。

线程安全共享。线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口进行访问而不需要进一步的同步。(例如 ConcurrentHashMap)

保护对象。被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值