虽然很少有 Java™ 开发人员能够忽视多线程编程和支持它的 Java 平台库,更少有人有时间深入研究线程。相反地,我们临时学习线程,在需要时向我们的工具箱添加新的技巧和技术。以这种方式构建和运行适当的应用程序是可行的,但是您可以做的不止这些。理解 Java 编译器的线程处理特性和 JVM 将有助于您编写更高效、性能更好的 Java 代码。

在这期的 5 件事 系列 中,我将通过同步方法、volatile 变量和原子类介绍多线程编程的一些更隐晦的方面。我的讨论特别关注于这些构建如何与 JVM 和 Java 编译器交互,以及不同的交互如何影响 Java 应用程序的性能。

1. 同步方法或同步代码块?

您可能偶尔会思考是否要同步化这个方法调用,还是只同步化该方法的线程安全子集。在这些情况下,知道 Java 编译器何时将源代码转化为字节代码会很有用,它处理同步方法和同步代码块的方式完全不同。

当 JVM 执行一个同步方法时,执行中的线程识别该方法的 method_info 结构是否有 ACC_SYNCHRONIZED 标记设置,然后它自动获取对象的锁,调用方法,最后释放锁。如果有异常发生,线程自动释放锁。

另一方面,同步化一个方法块会越过 JVM 对获取对象锁和异常处理的内置支持,要求以字节代码显式写入功能。如果您使用同步方法读取一个方法的字节代码,就会看到有十几个额外的操作用于管理这个功能。清单 1 展示用于生成同步方法和同步代码块的调用:

清单 1. 两种同步化方法

package com.geekcap;public class SynchronizationExample {private int i;public synchronized int synchronizedMethodGet() {return i;}public int synchronizedBlockGet() {synchronized( this ) {return i;}}
}

synchronizedMethodGet() 方法生成以下字节代码:

    0:  aload_01:   getfield2:  nop3:   iconst_m14: ireturn

这里是来自 synchronizedBlockGet() 方法的字节代码:

  0:  aload_01:   dup2:   astore_13:  monitorenter4:  aload_05:   getfield6:  nop7:   iconst_m18: aload_19:   monitorexit10:  ireturn11:  astore_212: aload_113:  monitorexit14:  aload_215:  athrow

创建同步代码块产生了 16 行的字节码,而创建同步方法仅产生了 5 行。

回页首

2. ThreadLocal 变量

如果您想为一个类的所有实例维持一个变量的实例,将会用到静态类成员变量。如果您想以线程为单位维持一个变量的实例,将会用到线程局部变量。ThreadLocal 变量与常规变量的不同之处在于,每个线程都有其各自初始化的变量实例,这通过 get() 或 set() 方法予以评估。

比方说您在开发一个多线程代码跟踪器,其目标是通过您的代码惟一标识每个线程的路径。挑战在于,您需要跨多个线程协调多个类中的多个方法。如果没有 ThreadLocal,这会是一个复杂的问题。当一个线程开始执行时,它需要生成一个惟一的令牌来在跟踪器中识别它,然后将这个惟一的令牌传递给跟踪中的每个方法。

使用 ThreadLocal,事情就变得简单多了。线程在开始执行时初始化线程局部变量,然后通过每个类的每个方法访问它,保证变量将仅为当前执行的线程托管跟踪信息。在执行完成之后,线程可以将其特定的踪迹传递给一个负责维护所有跟踪的管理对象。

当您需要以线程为单位存储变量实例时,使用 ThreadLocal 很有意义。

回页首

3. Volatile 变量

我估计,大约有一半的 Java 开发人员知道 Java 语言包含 volatile 关键字。当然,其中只有 10% 知道它的确切含义,有更少的人知道如何有效使用它。简言之,使用 volatile 关键字识别一个变量,意味着这个变量的值会被不同的线程修改。要完全理解 volatile关键字的作用,首先应当理解线程如何处理非易失性变量。

为了提高性能,Java 语言规范允许 JRE 在引用变量的每个线程中维护该变量的一个本地副本。您可以将变量的这些 “线程局部” 副本看作是与缓存类似,在每次线程需要访问变量的值时帮助它避免检查主存储器。

不过看看在下面场景中会发生什么:两个线程启动,第一个线程将变量 A 读取为 5,第二个线程将变量 A 读取为 10。如果变量 A 从 5 变为 10,第一个线程将不会知道这个变化,因此会拥有错误的变量 A 的值。但是如果将变量 A 标记为 volatile,那么不管线程何时读取 A 的值,它都会回头查阅 A 的原版拷贝并读取当前值。

如果应用程序中的变量将不发生变化,那么一个线程局部缓存比较行得通。不然,知道 volatile 关键字能为您做什么会很有帮助。

回页首

4. 易失性变量与同步化

如果一个变量被声明为 volatile,这意味着它预计会由多个线程修改。当然,您会希望 JRE 会为易失性变量施加某种形式的同步。幸运的是,JRE 在访问易失性变量时确实隐式地提供同步,但是有一条重要提醒:读取易失性变量是同步的,写入易失性变量也是同步的,但非原子操作不同步。

这表示下面的代码不是线程安全的:

myVolatileVar++;

上一条语句也可写成:

int temp = 0;
synchronize( myVolatileVar ) {temp = myVolatileVar;
}temp++;synchronize( myVolatileVar ) {myVolatileVar = temp;
}

换言之,如果一个易失性变量得到更新,这样其值就会在底层被读取、修改并分配一个新值,结果将是一个在两个同步操作之间执行的非线程安全操作。然后您可以决定是使用同步化还是依赖于 JRE 的支持来自动同步易失性变量。更好的方法取决于您的用例:如果分配给易失性变量的值取决于当前值(比如在一个递增操作期间),要想该操作是线程安全的,那么您必须使用同步化。

回页首

5. 原子字段更新程序

在一个多线程环境中递增或递减一个原语类型时,使用在 java.util.concurrent.atomic 包中找到的其中一个新原子类比编写自己的同步代码块要好得多。原子类确保某些操作以线程安全方式被执行,比如递增和递减一个值,更新一个值,添加一个值。原子类列表包括 AtomicIntegerAtomicBooleanAtomicLongAtomicIntegerArray 等等。

使用原子类的难题在于,所有类操作,包括 getset 和一系列 get-set 操作是以原子态呈现的。这表示,不修改原子变量值的 read和 write 操作是同步的,不仅仅是重要的 read-update-write 操作。如果您希望对同步代码的部署进行更多细粒度控制,那么解决方案就是使用一个原子字段更新程序。

使用原子更新

像 AtomicIntegerFieldUpdaterAtomicLongFieldUpdater 和 AtomicReferenceFieldUpdater 之类的原子字段更新程序基本上是应用于易失性字段的封装器。Java 类库在内部使用它们。虽然它们没有在应用程序代码中得到广泛使用,但是也没有不能使用它们的理由。

清单 2 展示一个有关类的示例,该类使用原子更新来更改某人正在读取的书目:

清单 2. Book 类

package com.geeckap.atomicexample;public class Book
{private String name;public Book(){}public Book( String name ){this.name = name;}public String getName(){return name;}public void setName( String name ){this.name = name;}
}

Book 类仅是一个 POJO(Java 原生类对象),拥有一个单一字段:name。

清单 3. MyObject 类

package com.geeckap.atomicexample;import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;/**** @author shaines*/
public class MyObject
{private volatile Book whatImReading;private static final AtomicReferenceFieldUpdater<MyObject,Book> updater =AtomicReferenceFieldUpdater.newUpdater( MyObject.class, Book.class, "whatImReading" );public Book getWhatImReading(){return whatImReading;}public void setWhatImReading( Book whatImReading ){//this.whatImReading = whatImReading;updater.compareAndSet( this, this.whatImReading, whatImReading );}
}

正如您所期望的,清单 3 中的 MyObject 类通过 get 和 set 方法公开其 whatAmIReading 属性,但是 set 方法所做的有点不同。它不仅仅将其内部 Book 引用分配给指定的 Book(这将使用 清单 3 中注释出的代码来完成),而是使用一个AtomicReferenceFieldUpdater

AtomicReferenceFieldUpdater

AtomicReferenceFieldUpdater 的 Javadoc 将其定义为:

对指定类的指定易失性引用字段启用原子更新的一个基于映像的实用程序。该类旨在用于这样的一个原子数据结构中:即同一节点的若干引用字段独立地得到原子更新。

在 清单 3 中,AtomicReferenceFieldUpdater 由一个对其静态 newUpdater 方法的调用创建,该方法接受三个参数:

  • 包含字段的对象的类(在本例中为 MyObject
  • 将得到原子更新的对象的类(在本例中是 Book
  • 将经过原子更新的字段的名称

这里真正的价值在于,getWhatImReading 方法未经任何形式的同步便被执行,而 setWhatImReading 是作为一个原子操作执行的。

清单 4 展示如何使用 setWhatImReading() 方法并断定值的变动是正确的:

清单 4. 演习原子更新的测试用例

package com.geeckap.atomicexample;import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;public class AtomicExampleTest
{private MyObject obj;@Beforepublic void setUp(){obj = new MyObject();obj.setWhatImReading( new Book( "Java 2 From Scratch" ) );}@Testpublic void testUpdate(){obj.setWhatImReading( new Book( "Pro Java EE 5 Performance Management and Optimization" ) );Assert.assertEquals( "Incorrect book name", "Pro Java EE 5 Performance Management and Optimization", obj.getWhatImReading().getName() );}}

参阅 参考资料 了解有关原子类的更多信息。

回页首

结束语

多线程编程永远充满了挑战,但是随着 Java 平台的演变,它获得了简化一些多线程编程任务的支持。在本文中,我讨论了关于在 Java 平台上编写多线程应用程序您可能不知道的 5 件事,包括同步化方法与同步化代码块之间的不同,为每个线程存储运用ThreadLocal 变量的价值,被广泛误解的 volatile 关键字(包括依赖于 volatile 满足同步化需求的危险),以及对原子类的错杂之处的一个简要介绍。参见 参考资料 部分了解更多内容。

参考资料

学习

  • 您不知道的 5 件事 ... :在本系列中发现关于 Java 平台您不知道的事情,本系列致力于将 Java 技术琐事变成有用的编程技巧。
  • “Code Tracing”(Steven Haines,InformIT,2010 年 8 月):了解使用 ThreadLocal 变量进行代码跟踪的更多内容。
  • “Java 字节码:了解字节码使你成为一个更好的程序员”(Peter Haggar,developerWorks,2001 年 7 月):一本介绍字节码次要领域的教程,包含展示同步方法和同步代码块之间区别的一个较早的例子。
  • Java 理论与实践:流行的原子”(Brian Goetz,developerWorks,2004 年 11 月):解释原子类如何支持用 Java 语言开发高度可伸缩的非阻塞算法。
  • Java 理论与实践: 并发在一定程度上使一切变得简单”(Brian Goetz,developerWorks,2002 年 11 月): 通过java.util.concurrent 包来为您提供指导。
  • 您不知道的 5 件事 ... java.util.concurrent,第 1 部分”(Ted Neward,developerWorks,2010 年 5 月):了解 5 个并发集合类,为您的并发编程需求改进标准集合类。
  • developerWorks Java technology 专区:这里有数百篇关于 Java 编程各个方面的文章。

讨论

  • 加入 developerWorks 中文社区。

关于作者

Steven Haines 是 ioko 的一名技术架构师,也是 GeekCap Inc 的创始人。在 Java 编程和性能分析方面,他写过 3 本书,以及上百篇文章和十几个白皮书。Steven 还在行业会议上发表演讲,比如 JBoss World 和 STPCon,而且他曾在加里佛尼亚大学欧文分校和 Learning Tree 大学教过 Java 编程,他居住在佛罗里达州奥兰多市。

建议

关于多线程编程您不知道的 5 件事 有关高性能线程处理的微妙之处相关推荐

  1. 71java并发编程不得不知道的几件事

    多线程编程从来都是一件比较困难的事情,调试多线程程序也相当困难,这种困难来自于线程对共享资源操作的复杂性 ( 包括对于资源操作的线程间的先后顺序 ) .对于 Java 来说,它封装了底层硬件和操作系统 ...

  2. 阴阳师服务器维护结界卡暂停吗,阴阳师BUFF暂停功能你不能不知道的六件事

    阴阳师BUFF暂停功能已经正式上线啦,哪些种类的加成可以暂停?在哪里找暂停按钮?暂停有时间限制吗?有次数限制吗?关于BUFF暂停你不能不知道的六件事! 一.在哪里暂停? 在庭院处点击头像右边的&quo ...

  3. 关于ERP系统,你可能不知道的10件事

    谈到ERP系统,大多数人只是考虑ERP日常管理的核心功能,即财务.销售.采购.库存.生产和分销.保持对这些关键领域的控制对任何企业的成功都是不可或缺的.但这些只是冰山一角,如果深入挖掘ERP系统,你可 ...

  4. 佳能eosr控制环能否计算机控制,关于全画幅微单相机 你可能不知道的十件事

    01关于微单相机你不知道的十件事 2018年之前,全画幅微单领域没有竞争,索尼一家独大,徕卡只走奢侈路线.为了满足专业摄影师和资深发烧玩家更多的使用需求,佳能.尼康和松下纷纷加入战局,在2018年开启 ...

  5. 关于PHP你可能不知道的10件事

    小编之前也曾报导过PHP开发人员容易忽略的几点精华,除了一些精华技术方法外,很多细微之处也是程序员们容易忽略的,下面我们为您总结了10个关于PHP你可能不知道的事情. 关于PHP更多内容,欢迎访问:P ...

  6. 我的世界java版移除猪灵了吗_我的世界:猪灵拥有三种类型?关于猪灵,你可能不知道的7件事...

    猪灵是1.16下界更新第二个快照版本20w07a加入的下界新生物,虽然上线时间已经有两个月之久,但对于大部分国内玩家来说却还是一个陌生的存在.为了让玩家更加的了解猪灵,迷恋整理了7件,你可能不知道的关 ...

  7. 关于 java.util.concurrent 您不知道的 5 件事--转

    第 1 部分 http://www.ibm.com/developerworks/cn/java/j-5things4.html Concurrent Collections 是 Java™ 5 的巨 ...

  8. 关于 Java Collections API 您不知道的 5 件事--转

    第 1 部分 http://www.ibm.com/developerworks/cn/java/j-5things2.html 对于很多 Java 开发人员来说,Java Collections A ...

  9. 关于 Java 对象序列化您不知道的 5 件事

    数年前,当和一个软件团队一起用 Java 语言编写一个应用程序时,我体会到比一般程序员多知道一点关于 Java 对象序列化的知识所带来的好处. 关于本系列 您觉得自己懂 Java 编程?事实上,大多数 ...

最新文章

  1. 拼多多面试|如何用 Redis 统计独立用户访问量?
  2. 后台开发经典书籍--深入理解计算机系统
  3. @ConditionalOnMissingBean注解理解
  4. 微信公众号,商城开发
  5. JAVA实现服务器间拷贝文件,寻找在Java服务器之间传输大文件的好方法
  6. 模拟3个人排除买票,每人买1张票。售货员只有1张五元的钱,电影票5元一张
  7. 第一次来到博客园.....
  8. 新疆职称英语和职称计算机,职称计算机考试题库
  9. 关于单链表中temp.next、head.next的理解
  10. phpcms修改了配置文件之后出现Warning: date() expects parameter 2 to be long..错误
  11. VB.net中金额大写转换
  12. MYSQL的简单查询
  13. if函数多个条件php,EXCEL中IF函数多条件判断怎么写
  14. 把oracle19c数据导入oracle11g
  15. 趣味算法一棋盘的麦子
  16. 机架惠普服务器型号,惠普HP DL160G6机架式服务器
  17. 百度-AI趣味课堂(三)
  18. 中缀、前缀和后缀表达式
  19. 联想S820 MIUI刷机包 MIUI 4.4.30 流畅运行 在线主题破解
  20. 计算机新教师汇报课报道,责任课堂促成长 初露头角展风采——江桥小学新进教师汇报展示课...

热门文章

  1. 云存储技术-JDK的安装
  2. linux brctl命令,Linux中brctl命令起什么作用呢?
  3. java获取文件新增内容_关于java生成文件,立即又读这个文件但又找不到文件新增内容的问题...
  4. android mysql实现登录注册_android简单登陆和注册功能实现+SQLite数据库学习
  5. retrofit 2.0 android 教程,初识Retrofit2.0
  6. php strchr 截断,PHP strchr() 函数
  7. 各层电子数排布规则_原子核外电子排布原理
  8. jav简单的个人博客网站代码_每个人都可以拥有的个人博客网站
  9. 简单java在线测评程序
  10. 惠普800g1支持什么内存_惠普黑白激光打印机哪种好 惠普黑白激光打印机推荐【图文详解】...