欢迎来真孝善网,为您提供真孝善正能量书籍故事!

深入探讨“final”关键字在多线程编程中的应用

时间:10-28 神话故事 提交错误

大家好,今天小编来为大家解答深入探讨“final”关键字在多线程编程中的应用这个问题,很多人还不知道,现在让我们一起来看看吧!

并发编程网:http://ifeve.com/java-memory-model/

总结:

并发中Final变量的原理是通过禁止CPU的指令集重新排序来提供现成的变量(重新排序详细解释http://ifeve.com/java-memory-model-1/http://ifeve.com/java-memory-model-2 /) 课件保证对象的安全释放,防止对象引用在对象完全构造之前被其他线程获取和使用。

与前面介绍的锁和易失性相比,读写final字段更像是普通的变量访问。对于最终字段,编译器和处理器必须遵守两个重新排序规则:

写入构造函数中的Final 字段并随后将对构造对象的引用分配给引用变量无法重新排序。对包含Final 字段的对象的引用的第一次读取以及对Final 字段的后续第一次读取无法重新排序。它与Volatile有类似的功能,但Final主要用于不可变变量(基本数据类型和非基本数据类型)的安全释放(初始化)。 Volatile可以用来安全地发布不可变变量,也可以提供可变变量的可见性。

安全发布的常用模式

可变对象必须以安全方式发布,这通常意味着在发布和使用使用该对象的线程时都必须使用同步。现在,我们将重点关注如何确保使用对象的线程可以看到该对象处于已发布状态,稍后将重点讨论如何在发布后修改对象的可见性。

为了安全地发布对象,对象的应用程序和对象的状态必须同时对其他线程可见。正确构造的对象可以通过以下方式安全地发布:

在静态初始化函数中初始化对象引用。将对象的应用程序保存在易失性类型字段或AtomicReferance 对象中。将对象的引用保存在正确构造的对象的最终类型字段中。将对象的引用保存在受锁保护的字段中。在域中。线程安全容器内的同步意味着当将对象放入容器(例如Vector 或synchronizedList)时,满足上述最后一个要求。如果线程A 将对象X 放入线程安全容器中,然后线程B 读取该对象,则可以保证B 看到样式同步。尽管Javadoc对此主题不是很清楚,但线程安全库中的容器类提供了以下安全释放保证:

通过将键或值放入Hashtable、synchronizedMap 或ConcurrentMap,您可以安全地将其发布到从这些容器访问它的任何线程(直接或通过迭代器)。通过将元素放入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList 或synchronizedSet,可以将该元素安全地发布到从这些容器访问该元素的任何线程。通过将元素放入BlockingQueue 或ConcurrentLinkedQueue 中,可以将该元素安全地发布到从这些容器访问该元素的任何线程。这些队列中访问元素的线程。类库中的其他数据传输机制(例如Future和Exchanger)也可以实现安全发布,它们的安全发布能力将在介绍这些机制时讨论。

通常,发布静态构造对象的最简单、最安全的方法是使用静态初始化器: public static Holderholder=new Holder(42);

静态初始化程序由JVM 在类的初始化阶段执行。由于JVM 内的同步机制,以这种方式初始化的任何对象都可以安全地释放[JLS 12.4.2]。

详情如下:

一、不变性

满足同步需求的另一种方法是使用不可变对象(Immutable Object)。到目前为止,我们已经介绍了许多与原子性和可见性相关的问题,例如获取无效数据、丢失更新操作,或者只是发现某个对象处于不一致的状态等,所有这些都是多线程视图访问相同的可用状态同时。与变更后的状态相关。如果对象的状态不改变,那么这些问题和复杂性就会消失。

如果一个对象的状态在创建后就无法修改,那么该对象称为不可变对象。线程安全是不可变对象的固有属性之一。它们的不变性条件是构造函数创建。只要它们的状态不改变,这些不变性条件就可以维持。

不可变对象很简单。它们只有一种状态,并且该状态由构造函数控制。编程最困难的方面之一是确定复杂对象的可能状态。然而,确定不可变对象的状态很简单。

尽管Java规范和Java内存模型中没有关于不变性的正式定义,但不变性并不意味着将对象中的所有字段都声明为最终类型。即使对象中的所有字段都是最终类型,该对象也仍然是可变的,因为对可变对象的引用可以保存在最终类型的字段中。

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

对象创建后,其状态无法修改。该对象的所有对象在为final类型对象时都被正确创建(创建过程中没有this的转义)。下面我们来分析一下这个类。

@不可变

公共最终课ThreeStooges {

私有最终Setstooges=new HashSet();

公共ThreeStooges(){

stooges.add("Moe");

傀儡.add("拉里");

stooges.add("卷曲");

}

公共布尔isStooge(字符串名称){

return stooges.contains(name);

}

}

可变对象仍然可以在不可变对象内部使用来管理它们的状态,如ThreeStooges 所示。虽然保存名称的Set对象是可变的,但是从ThreeStooges的设计中可以看出,Set对象构造完成后就无法修改。 Stooges 是一个最终引用变量,因此所有对象状态都通过最终字段访问。最后一个要求是“正确构造对象”,这很容易满足,因为构造函数使引用可以被构造函数及其调用者之外的代码访问。

由于程序的状态不断变化,您可能会认为需要使用不可变对象的地方并不多,但事实并非如此。 “不可变对象”和“不可变对象引用”之间是有区别的。存储在不可变对象中的程序状态仍然可以通过用存储新状态的实例“替换”原始不可变对象来更新。

Final 域

关键字final可以被认为是C++中用于构造不可变对象的const机制的受限版本。 Final类型的字段不能修改(但是如果final字段引用的对象是可变的,那么这些引用的对象可以修改)。然而,在Java内存模型中,final字段具有特殊的语义。 Final字段确保了初始化过程的安全性,允许不受限制地访问不可变对象,并且在共享这些对象时无需同步。

注:我个人的理解是,一旦final字段被初始化,并且构造函数没有将这个引用传出去,那么final字段的值就可以在其他线程中看到(域中变量的可见性,类似于volatile),其external 可见状态永远不会改变。它带来的安全感是最简单、最纯粹的。

注意:即使对象是可变的,通过将对象的某些字段声明为final类型,仍然可以是简化对状态的判断,所以限制对象的可变性相当于限制对象可能状态的集合。仅包含一两个可变状态的“基本不可变”对象仍然比包含多个可变状态的对象更简单。通过将字段声明为最终字段,您还告诉维护者这些字段不会更改。

正如“除非需要更高的可见性,否则将所有字段声明为私有”[EJ 第12 条] 是一个很好的经验法则,“除非需要一个字段是可变的,否则应该“将其声明为最终字段”也是一个很好的经验法则习惯。

示例:使用 Volatile 类型来发布不可变对象

之前我们说过,使用volatile 可以保证字段的可见性,但不能保证变量操作的原子性。更准确的说,它只能保证读写操作的原子性,而不能保证自增i++等操作。操作的原子性。

在前面的UnsafeCachingFactorizer 类中,我们尝试使用两个AtomicReferences 变量来保存最新值及其分解结果,但此方法不是线程安全的,因为我们无法同时原子地读取或更新这两个变量。相关值。同样,使用volatile 变量来存储这些值也不是线程安全的。然而,在某些情况下,不可变对象可以提供弱形式的原子性。

分解servlet会执行两个原子操作:更新缓存的结果,并通过判断缓存中的值是否等于请求的值来决定是否直接读取缓存中的分解结果。每当您需要对一组相关数据以原子方式执行操作时,请考虑创建一个不可变类来包含数据,例如OneValueCache。

@不可变

类OneValueCache {

私有最终BigInteger LastNumber;

私有最终BigInteger[] lastFactors;

/**

如果构造函数中没有使用Arrays.copyOf()方法,那么域中的不可变对象lastFactors可以被域外的代码更改,因此OneValueCache不是不可变的。

*/

公共OneValueCache(BigInteger I,

BigInteger[] 因子) {

最后一个数字=I;

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

}公共BigInteger[] getFactors(BigInteger i) {

if (lastNumber==null || !lastNumber.equals(i))

返回空值;

别的

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

}

}

通过将这些变量全部保存在一个不可变对象中,可以消除访问和更新多个相关变量时出现的竞争条件问题。如果是可变对象,那么必须使用锁来保证原子性。如果是不可变对象,那么当线程获得该对象的引用时,就不需要担心另一个线程修改该对象的状态。如果要更新这些变量,可以创建一个新的容器对象,但使用原始对象的其他线程仍会看到该对象处于一致状态。

VolatileCachedFactorizer 中使用OneValueCache 来保存缓存值及其因子。我们将OneValueCache 声明为volatile,这样当一个线程将缓存设置为引用新的OneValueCache 时,其他线程将立即看到新缓存的数据。

@ThreadSafe

公共类VolatileCachedFactorizer 实现Servlet {

私有易失性OneValueCache 缓存=

新的OneValueCache(null, null);

公共无效服务(ServletRequest req,ServletResponse resp){

BigInteger i=extractFromRequest(req);

BigInteger[] Factors=cache.getFactors(i);

如果(因素==空){

因子=因子(i);

cache=new OneValueCache(i, Factors);//声明为易失性,以防止指令重排序并保证可见性

}

编码成响应(分别,因子);

}

}

与缓存相关的操作不会相互干扰,因为OneValueCache 是不可变的,并且在每个相应的代码路径中仅访问一次。通过使用包含多个状态变量的容器对象来维护不变性条件,并使用易失性引用来确保可见性,易失性缓存分解器仍然是线程安全的,无需显式使用锁。

二、安全发布

到目前为止,我们重点关注如何确保对象不被释放,例如将对象封闭在线程或另一个对象中。当然,有些情况我们希望在多个线程之间共享对象,在这种情况下我们必须确保共享是安全完成的。但是,仅将对象引用保存到公共域(如以下程序所示)不足以安全地发布对象。

//不安全释放

公众持有人持有人;

公共无效初始化(){

持有者=新持有者(42);

}

您可能想知道为什么这个看似很好的例子失败了。由于可见性问题,其他线程看到的Holder对象将处于不一致的状态,即使对象的构造函数中已经正确构造了不变性条件。这种不正确的发布会导致其他线程看到尚未创建的对象。

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

您不能期望尚未完全创建的对象具有完整性。观察该对象的线程将看到该对象处于不一致的状态,然后看到该对象的状态突然发生变化,即使该线程自释放以来尚未修改该对象。事实上,如果下面程序中的Holder使用了前面程序中不安全的释放方法,那么另一个线程在调用assertSanity时就会抛出AssertionError。

公开课持有者{

私有int n;

公共持有者(int n){ this.n=n; }

公共无效assertSanity(){

如果(n!=n)

throw new AssertionError("这个陈述是错误的。");

}

}

因为没有使用同步来确保Holder对象对其他线程可见,所以Holder被称为“未正确释放”。未正确发布的对象存在两个问题。

**首先**,Holder域是一个失效值对发布对象的线程以外的线程可见,因此将看到空引用或之前的旧值。

** 然而**,更糟糕的情况是线程看到Holder引用的值是最新的,但是Holder状态的值是无效的。使情况变得更加不可预测的是,线程第一次读取该字段时获得无效值,并在下次读取该字段时获得更新的值,这就是assertSainty抛出AssertionError的原因。

如果没有足够的同步,当多个线程之间共享数据时,就会发生一些非常奇怪的事情。

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

由于不可变对象是一种非常重要的对象,因此Java内存模型为不可变对象的共享提供了一种特殊的初始化安全性保证。我们已经知道,即使对象的引用对其他线程可见,并不意味着该对象状态对使用该对象的线程一定是可见的。为了确保对象状态视图的一致性,必须使用同步。

另一方面,即使在发布对不可变对象的引用时不使用同步,访问该对象仍然是安全的。为了维持初始化安全性的保证,必须满足不变性的所有要求:状态不能修改,所有字段都是最终的,并且构造是正确的。 (如果Holder对象是不可变的,那么即使Holder没有被正确释放,在assertSanity中也不会抛出AssertionError。)

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

此保证还扩展到正确创建的对象中的所有最终字段。无需额外同步即可安全访问最终类型字段。但是,如果final字段指向可变对象,则在访问这些字段所指向的对象的状态时仍然需要同步。

安全发布的常用模式

可变对象必须以安全方式发布,这通常意味着在发布和使用使用该对象的线程时都必须使用同步。现在,我们将重点关注如何确保使用对象的线程可以看到该对象处于已发布状态,稍后将重点讨论如何在发布后修改对象的可见性。

为了安全地发布对象,对象的应用程序和对象的状态必须同时对其他线程可见。正确构造的对象可以通过以下方式安全地发布:

在静态初始化函数中初始化对象引用。将对象的应用程序保存在易失性类型字段或AtomicReferance 对象中。将对象的引用保存在正确构造的对象的最终类型字段中。将对象的引用保存在受锁保护的字段中。在域中。线程安全容器内的同步意味着当将对象放入容器(例如Vector 或synchronizedList)时,满足上述最后一个要求。如果线程A 将对象X 放入线程安全容器中,然后线程B 读取该对象,则可以保证B 看到样式同步。尽管Javadoc对此主题不是很清楚,但线程安全库中的容器类提供了以下安全释放保证:

通过将键或值放入Hashtable、synchronizedMap 或ConcurrentMap,您可以安全地将其发布到从这些容器访问它的任何线程(直接或通过迭代器)。通过将元素放入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList 或synchronizedSet,可以将该元素安全地发布到从这些容器访问该元素的任何线程。通过将元素放入BlockingQueue 或ConcurrentLinkedQueue 中,可以将该元素安全地发布到从这些容器访问该元素的任何线程。这些队列中访问元素的线程。类库中的其他数据传输机制(例如Future和Exchanger)也可以实现安全发布,它们的安全发布能力将在介绍这些机制时讨论。

通常,发布静态构造对象的最简单、最安全的方法是使用静态初始化器: public static Holderholder=new Holder(42);

静态初始化程序由JVM 在类的初始化阶段执行。由于JVM 内的同步机制,以这种方式初始化的任何对象都可以安全地释放[JLS 12.4.2]。

事实不可变对象

如果对象在发布后不会被修改,则安全发布足以让其他线程安全地访问这些对象,而无需额外同步。所有安全释放机制都保证当一个对象的引用对所有访问该对象的线程都可见时,该对象被释放时的状态也将对所有线程可见,并且如果该对象状态不会再次改变,那么它就被释放了。足以确保任何访问都是安全的。

如果一个对象在技术上是可变的,但其状态在发布后不会改变,那么这种类型的对象被称为“事实不可变对象(有效不可变对象)”。这些对象不需要满足之前提出的不变性的严格定义。这些对象发布后,程序只是将它们视为不可变对象。通过使用事实上的不可变对象,您不仅可以简化开发过程,还可以由于减少同步而提高性能。

安全发布的事实上的不可变对象可以被任何线程安全地使用,而无需额外的同步。

例如,Date 本身是可变的,但如果将其用作不可变对象,则可以在多个线程之间共享Date 对象时节省锁的使用。假设您需要维护一个存储每个用户最近登录时间的Map对象: public MaplastLogin=Collections.synchronizedMap(new HashMap());

如果一个Date对象的值放入Map后不会改变,那么synchronizedMap中的同步机制就足以让Date值安全发布,访问这些Date值时不需要额外的同步。

可变对象

如果对象在构造后可以修改,则安全发布仅确保“发布时”状态的可见性。对于可变对象,不仅在发布对象时需要同步,而且每次访问对象时都需要使用同步,以保证后续修改操作的可见性。为了安全地共享可变对象,这些对象必须被安全地释放,并且必须是线程安全的或受锁保护。

对象的发布要求取决于其可变性:

用户评论

敬情

final 关键字真挺管用的,能保证变量和方法的稳定性。

    有12位网友表示赞同!

愁杀

在多线程环境下使用 final 关键字显得特别重要,防止并发修改导致的问题。

    有7位网友表示赞同!

青墨断笺み

学到一个新东西了,final 可以用来修饰类、变量和方法的!

    有9位网友表示赞同!

浮光浅夏ζ

多线程 programming 确实容易出bug,final 能帮我们降低风险。

    有15位网友表示赞同!

话扎心

这个 final 关键字真像个“锁”,把数据保护得更牢固。

    有10位网友表示赞同!

醉婉笙歌

之前代码冲突老是因为数据并发修改,现在试试 final 吧!

    有14位网友表示赞同!

♂你那刺眼的温柔

多线程编程确实很复杂,final 能给我的程序加一层保障。

    有13位网友表示赞同!

白恍

终于明白为什么 final 在多线程中很重要了!

    有14位网友表示赞同!

轨迹!

看了这篇博客,感觉 learn 到很多关于 final 和多线程的知识。

    有15位网友表示赞同!

。婞褔vīp

希望以后遇到多线程问题的时候可以记得用 final 关键字解决。

    有19位网友表示赞同!

疲倦了

final 的作用很明显啊,能防止错误变更和不合理操作。

    有14位网友表示赞同!

夏至离别

学习使用 final 多线程编程技术,让我的程序更加健壮!

    有7位网友表示赞同!

月下独酌

文章讲解得很详细,让我快速理解了 final 在多线程中的应用。

    有11位网友表示赞同!

非想

多线程的安全性问题,final 能提供有效的解决方法。

    有16位网友表示赞同!

咆哮

有了 final 关键字,再也不用担心多线程下的数据不一致问题了!

    有5位网友表示赞同!

执妄

学习完这篇博客,感觉对 final 和多线程编程更有了解了。

    有12位网友表示赞同!

﹏櫻之舞﹏

多线程调试确实很麻烦,final 能让我们写出更稳健的代码!

    有7位网友表示赞同!

迁心

对于初学者来说,学习 final 关键字和多线程编程还是很有挑战性的!

    有7位网友表示赞同!

歇火

感谢作者分享学习资料,让我更深入地理解了 final 和多线程!

    有20位网友表示赞同!

【深入探讨“final”关键字在多线程编程中的应用】相关文章:

1.蛤蟆讨媳妇【哈尼族民间故事】

2.米颠拜石

3.王羲之临池学书

4.清代敢于创新的“浓墨宰相”——刘墉

5.“巧取豪夺”的由来--米芾逸事

6.荒唐洁癖 惜砚如身(米芾逸事)

7.拜石为兄--米芾逸事

8.郑板桥轶事十则

9.王献之被公主抢亲后的悲惨人生

10.史上真实张三丰:在棺材中竟神奇复活