电脑故障

位置:IT落伍者 >> 电脑故障 >> 浏览文章

多线程编程您不知道的5件事


发布日期:2021/5/23
 
多线程编程向来不容易但很少有 Java? 开发人员能够忽视多线程编程和支持它的 Java 平台库我们临时学习线程在需要时向我们的工具箱添加新的技巧和技术以这种方式构建和运行适当的应用程序是可行的但是您可以做的不止这些理解 Java 编译器的线程处理特性和 JVM 将有助于您编写更高效性能更好的 Java 代码

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

同步方法或同步代码块?

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

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

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

清单 两种同步化方法

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

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

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

ThreadLocal 变量

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

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

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

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

Volatile 变量

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

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

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

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

易失性变量与同步化

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

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

myVolatileVar++;

上一条语句也可写成

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

原子字段更新程序

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

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

使用原子更新

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

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

清单 Book 类

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

清单 MyObject 类

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

AtomicReferenceFieldUpdater

AtomicReferenceFieldUpdater 的 Javadoc 将其定义为

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

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

包含字段的对象的类(在本例中为 MyObject)

将得到原子更新的对象的类(在本例中是 Book)

将经过原子更新的字段的名称

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

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

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

结束语

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

上一篇:爪哇语言抽象工厂创立性模式介绍

下一篇:1100行代码设计一个线程池