简介: 多线程编程向来不容易,但是它确实有助于理解 JVM 进程如何巧妙地构建不同代码。Steven Haines 分享 5 个技巧,可以帮助您在使用同步方法、volatile 变量和原子类时做出更明智的决策。
虽然很少有 Java™ 开发人员能够忽视多线程编程和支持它的 Java 平台库,更少有人有时间深入研究线程。相反地,我们临时学习线程,在需要时向我们的工具箱添加新的技巧和技术。以这种方式构建和运行适当的应用程序是可行的,但是您可以做的不止这些。理解 Java 编译器的线程处理特性和 JVM 将有助于您编写更高效、性能更好的 Java 代码。
在这期的 5 件事 系列 中,我将通过同步方法、volatile 变量和原子类介绍多线程编程的一些更隐晦的方面。我的讨论特别关注于这些构建如何与 JVM 和 Java 编译器交互,以及不同的交互如何影响 Java 应用程序的性能。
您可能偶尔会思考是否要同步化这个方法调用,还是只同步化该方法的线程安全子集。在这些情况下,知道 Java 编译器何时将源代码转化为字节代码会很有用,它处理同步方法和同步代码块的方式完全不同。
当 JVM 执行一个同步方法时,执行中的线程识别该方法的 method_info
结构是否有 ACC_SYNCHRONIZED
标记设置,然后它自动获取对象的锁,调用方法,最后释放锁。如果有异常发生,线程自动释放锁。
另一方面,同步化一个方法块会越过 JVM 对获取对象锁和异常处理的内置支持,要求以字节代码显式写入功能。如果您使用同步方法读取一个方法的字节代码,就会看到有十几个额外的操作用于管理这个功能。清单 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_0
1: getfield
2: nop
3: iconst_m1
4: ireturn
|
这里是来自 synchronizedBlockGet()
方法的字节代码:
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: aload_0
5: getfield
6: nop
7: iconst_m1
8: aload_1
9: monitorexit
10: ireturn
11: astore_2
12: aload_1
13: monitorexit
14: aload_2
15: athrow
|
创建同步代码块产生了 16 行的字节码,而创建同步方法仅产生了 5 行。
如果您想为一个类的所有实例维持一个变量的实例,将会用到静态类成员变量。如果您想以线程为单位维持一个变量的实例,将会用到线程局部变量。ThreadLocal
变量与常规变量的不同之处在于,每个线程都有其各自初始化的变量实例,这通过 get()
或 set()
方法予以评估。
比方说您在开发一个多线程代码跟踪器,其目标是通过您的代码惟一标识每个线程的路径。挑战在于,您需要跨多个线程协调多个类中的多个方法。如果没有 ThreadLocal
,这会是一个复杂的问题。当一个线程开始执行时,它需要生成一个惟一的令牌来在跟踪器中识别它,然后将这个惟一的令牌传递给跟踪中的每个方法。
使用 ThreadLocal
,事情就变得简单多了。线程在开始执行时初始化线程局部变量,然后通过每个类的每个方法访问它,保证变量将仅为当前执行的线程托管跟踪信息。在执行完成之后,线程可以将其特定的踪迹传递给一个负责维护所有跟踪的管理对象。
当您需要以线程为单位存储变量实例时,使用 ThreadLocal
很有意义。
我估计,大约有一半的 Java 开发人员知道 Java 语言包含 volatile
关键字。当然,其中只有 10% 知道它的确切含义,有更少的人知道如何有效使用它。简言之,使用 volatile
关键字识别一个变量,意味着这个变量的值会被不同的线程修改。要完全理解 volatile
关键字的作用,首先应当理解线程如何处理非易失性变量。
为了提高性能,Java 语言规范允许 JRE 在引用变量的每个线程中维护该变量的一个本地副本。您可以将变量的这些 “线程局部” 副本看作是与缓存类似,在每次线程需要访问变量的值时帮助它避免检查主存储器。
不过看看在下面场景中会发生什么:两个线程启动,第一个线程将变量 A 读取为 5,第二个线程将变量 A 读取为 10。如果变量 A 从 5 变为 10,第一个线程将不会知道这个变化,因此会拥有错误的变量 A 的值。但是如果将变量 A 标记为 volatile
,那么不管线程何时读取 A 的值,它都会回头查阅 A 的原版拷贝并读取当前值。
如果应用程序中的变量将不发生变化,那么一个线程局部缓存比较行得通。不然,知道 volatile
关键字能为您做什么会很有帮助。
如果一个变量被声明为 volatile
,这意味着它预计会由多个线程修改。当然,您会希望 JRE 会为易失性变量施加某种形式的同步。幸运的是,JRE 在访问易失性变量时确实隐式地提供同步,但是有一条重要提醒:读取易失性变量是同步的,写入易失性变量也是同步的,但非原子操作不同步。
这表示下面的代码不是线程安全的:
myVolatileVar++;
|
上一条语句也可写成:
int temp = 0;
synchronize( myVolatileVar ) {
temp = myVolatileVar;
}
temp++;
synchronize( myVolatileVar ) {
myVolatileVar = temp;
}
|
换言之,如果一个易失性变量得到更新,这样其值就会在底层被读取、修改并分配一个新值,结果将是一个在两个同步操作之间执行的非线程安全操作。然后您可以决定是使用同步化还是依赖于 JRE 的支持来自动同步易失性变量。更好的方法取决于您的用例:如果分配给易失性变量的值取决于当前值(比如在一个递增操作期间),要想该操作是线程安全的,那么您必须使用同步化。
在一个多线程环境中递增或递减一个原语类型时,使用在 java.util.concurrent.atomic
包中找到的其中一个新原子类比编写自己的同步代码块要好得多。原子类确保某些操作以线程安全方式被执行,比如递增和递减一个值,更新一个值,添加一个值。原子类列表包括 AtomicInteger
、AtomicBoolean
、AtomicLong
、AtomicIntegerArray
等等。
使用原子类的难题在于,所有类操作,包括 get
、set
和一系列 get-set
操作是以原子态呈现的。这表示,不修改原子变量值的 read
和 write
操作是同步的,不仅仅是重要的 read-update-write
操作。如果您希望对同步代码的部署进行更多细粒度控制,那么解决方案就是使用一个原子字段更新程序。
像 AtomicIntegerFieldUpdater
、AtomicLongFieldUpdater
和 AtomicReferenceFieldUpdater
之类的原子字段更新程序基本上是应用于易失性字段的封装器。Java 类库在内部使用它们。虽然它们没有在应用程序代码中得到广泛使用,但是也没有不能使用它们的理由。
清单 2 展示一个有关类的示例,该类使用原子更新来更改某人正在读取的书目:
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。
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
的 Javadoc 将其定义为:
对指定类的指定易失性引用字段启用原子更新的一个基于映像的实用程序。该类旨在用于这样的一个原子数据结构中:即同一节点的若干引用字段独立地得到原子更新。
在 清单 3 中,AtomicReferenceFieldUpdater
由一个对其静态 newUpdater
方法的调用创建,该方法接受三个参数:
- 包含字段的对象的类(在本例中为
MyObject
) - 将得到原子更新的对象的类(在本例中是
Book
) - 将经过原子更新的字段的名称
这里真正的价值在于,getWhatImReading
方法未经任何形式的同步便被执行,而 setWhatImReading
是作为一个原子操作执行的。
清单 4 展示如何使用 setWhatImReading()
方法并断定值的变动是正确的:
package com.geeckap.atomicexample;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
public class AtomicExampleTest
{
private MyObject obj;
@Before
public void setUp()
{
obj = new MyObject();
obj.setWhatImReading( new Book( "Java 2 From Scratch" ) );
}
@Test
public 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
满足同步化需求的危险),以及对原子类的错杂之处的一个简要介绍。参见 参考资料 部分了解更多内容。
相关推荐
在VC++编程环境中,多线程和进程...通过实际操作和调试这些代码,开发者可以更好地理解多线程和进程编程的复杂性和微妙之处,提升自己的编程技能。记得在实践中注意线程安全和性能优化,这是编写高效并发程序的关键。
在VC++编程环境中,微妙级定时(Microsecond-level Timing)是一项关键的技术,它涉及到精确的时间控制,对于实时系统、游戏开发、科学计算以及各种需要精确时间间隔的应用来说尤为重要。在这个"VC 微妙级定时的VC++...
- 多线程编程 - XML处理等。 **3.《Effective Java》** - **作者:** Joshua Bloch - **主要内容:** 这本书提供了许多关于如何编写高质量、可维护的Java代码的最佳实践建议。 - **涵盖知识点:** - 设计模式 ...
另一类常见问题是"死锁",这在多线程编程中尤为突出。当两个或更多的线程相互等待对方释放资源时,就会出现死锁。解决死锁的方法包括避免循环等待、设置超时、使用死锁预防或死锁避免算法等。 此外,还有性能优化...
需要注意的是,虽然`QueryPerformanceCounter()`提供了高精度,但它并不是线程安全的,因此在多线程环境下使用时,需要确保适当的同步措施。 在实际应用中,这样的计时器不仅可以用于性能基准测试,找出程序中的...
总的来说,无锁等待自由的循环FIFO队列是多线程编程中的一种强大工具,它可以提供高性能的并发访问,但同时也需要开发者具备深厚的并发理论基础和实践经验。理解和掌握这类技术,对于提升并发程序的设计水平和优化...
多线程编程中的同步问题、死锁和活锁是常见的困惑点。 7. **反射和动态代理**:反射允许程序在运行时检查和修改类、接口、字段和方法。动态代理则可以创建在运行时定义的新类型。这些高级特性在某些谜题中可能被...
在Java编程语言中,多线程和并发处理是核心特性,尤其在开发高效、响应迅速的应用时至关重要。本文将深入探讨Java中多线程的概念、并发问题以及如何解决这些问题。 一、多线程基础 多线程是指在一个程序中同时执行...
多线程技术是计算机编程中的一个关键概念,特别是在并发处理和优化系统性能方面。...总的来说,多线程编程是复杂且微妙的,需要理解其原理并熟练掌握同步、通信、资源管理等技术,才能编写出高效、稳定的多线程程序。
4. **进程与线程管理**:本书提供了关于进程创建、调度、同步和通信的详尽指导,同时涵盖了多线程编程的基础知识和最佳实践,帮助开发者构建高性能的并发应用。 5. **网络编程**:针对网络编程部分,本书覆盖了套接...
虽然Python提供了`threading`模块,但由于GIL(全局解释器锁),多线程在Python中并不能实现真正的并行计算,这在需要高性能计算的场景中需要注意。 9. **Python的迭代器与生成器** 迭代器和生成器是Python中高效...
"Puzzlers"在这里指的是书中精心设计的一系列编程谜题,通过这些谜题,开发者可以了解到Java语言中的陷阱和微妙之处。 1. **类型转换与强制转换**:Java中存在自动类型转换和显式强制类型转换,但过度依赖这些转换...
在现代计算机图形处理和高性能计算领域,OpenCL和CUDA是两种广泛使用的并行计算框架。CUDA由NVIDIA公司推出,主要用于其GPU(图形处理器)的编程,而OpenCL则是一个开放标准,适用于多种硬件平台,包括AMD、Intel和...
通过解决哲学家问题,我们可以学习到如何在多线程环境中优雅地管理并发和同步,这些技能对于任何涉及多线程编程的IT专业人士来说都是至关重要的。 总的来说,进程线程化是一个复杂而微妙的主题,涉及操作系统、编程...
在C/C++编程中,获取系统的毫秒、微秒或纳秒级时间差是常见的需求,特别是在性能测试、定时任务或者高精度计时场景中。...在多线程和高性能计算环境中,`QueryPerformanceCounter()` 通常更为可靠。
总之,Java编程中有很多微妙而重要的细节需要注意,包括异常处理的正确使用、源码的学习、开发工具的掌握以及相关资源的利用。通过深入理解这些知识点,我们可以提升编程水平,避免常见问题,从而编写出更加健壮、...
- **线程**:Java支持多线程编程,线程是程序中的执行流,可以提高程序效率。 - **同步机制**:synchronized关键字、wait()、notify()和notifyAll()用于线程间的同步与通信,避免竞态条件。 5. **集合框架**: -...
6. 性能优化:由于像素操作通常涉及大量的循环,为了提高效率,可以考虑使用多线程并行处理不同部分的图像,或者利用Java 8及更高版本的Stream API来简化代码。 7. 控制马赛克强度:通过调整马赛克块的大小,可以...