你所不知道的五件事情--多线程编程
这是IBM developerWorks中5 things系列文章中的一篇,讲述了关于多线程的一些应用窍门,值得大家学习。(2010.11.22最后更新)
摘要:多线程编程不轻松,但它确实能帮助理解JVM如何细微地处理不同代码结构。Steven Haines将分享的5个窍门会帮助你在处理同步方法,volatile变量以及原子类时做出更为合理的决定。
尽管很少有Java开发者能够忽略多线程编程,且Java平台类库支持它,甚至于更少的开发者能有时间去深入学习线程。相反,我们只是泛泛地学习线程,如果需要的话,会向我们的工具箱中添加新的技巧和技术。通过这种方法你可能会构建且运行好的应用程序,但你还能做得更好。理解Java编译器和JVM的线程特性,可以帮助你编写更高效,性能更佳的Java代码。
在5 things系列的本期文章中,我会介绍一些使用同步方法,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_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行代码。
2. ThreadLocal变量
如果你想为一个类的所有实例维护单个变量实例,你将使用静态类成员变量来实现这一点。如果你想在每个线程中维护一个变量的实例,你将使用thread- local变量。ThreadLocal变量不同于平常的变量,在于每个线程有它自己的变量初始化实例,通过get()或set()方法可以访问这些变量。
让我们说,你正在开发多线程代码追踪器的目的是从你的程序去唯一地标识每个线程的路径。挑战在于你需要在跨越多个线程的多个类中协调多个方法。没有 ThreadLocal,这将是一个很复杂的问题。当一个线程开始执行时,它将生成一个唯一的标记以便于在追踪器中进行标识,并在在路径中将这个唯一标记传给每个方法。
使用ThreadLocal,问题就变得简单了。线程在运行的开始时初始化thread-local变量,然后在各个类的各个方法中去访问它,这就能确保该变量只会在当前执行线程中维护路径信息。当线程执行完毕时,线程会将它的特定路径传递给一个管理对象,该对象负责维护所有的路径。
当你需要基于每个线程来存储变量时,使用ThreadLocal就很有意义。
3. volatile变量
我估计一大半Java开发员知道Java语言含有关键字volatile。其中大约只有10%的人知道它的意义,只有更少的人知道如何高效地使用它。简言之,将一个变量使用volatile关键字进行标识就意味着该变量的值将被不同的线程修改。为了充分理解volatile关键字的功用,首先就会帮助我们理解线程是如何处理非volatile变量的。
为了改进性能,Java语言规范允许JRE在各个线程中维护一份针对某个变量的引用的复本。你能够认为这些变量的"thread-local"复本类似于缓存,这会帮助线程避免在每次需要访问该变量的值时都去检查主内存。
但考虑下面场景可能会发生的事情:两个线程都启动了,第一个线程读到变量A的值为5,而第二个线程读到变量A的值为10。如果变量A已经从5变到10了,然后第一个线程并不会意识到这一变化,所以它会得到A的错误值。如果变量A被标记为volatile,然后在任何时候,某个线程读取A的值时,它都将查询 A的主复本并读到它的当前值。
如果应用中的变量不会改变,那么使用一个thread-local缓存将是有意义的。另外,知道volatile关键字能为你做些什么也是很有帮助的。
4. volatile对于同步
如果变量被声明为volatile,就意味着它会被多个线程所修改。很自然地,你会希望JRE能为volatile变量以某种方式强制执行同步。幸运地是,当访问volatile变量时,JRE隐式地提供了同步,但会伴随一个很大的代价:读volatile变量是同步的,写volatile变量也是同步的,但非原子性操作不能怎么做。
这就意味着下面的代码不是线程安全的:
myVolatileVar++;
前面的语句可以写成如下形式:
int temp = 0;
synchronize( myVolatileVar ) {
temp = myVolatileVar;
}
temp++;
synchronize( myVolatileVar ) {
myVolatileVar = temp;
}
换言之,如果一个volatile变量按上述方法来进行更新,即先读取值,并修改之,然后再赋值,在两个同步操作之间,这个结果是非线程安全的。你可以考虑是使用同步,还是依赖JRE对volatile变量的自动同步。更好的方法是根据你的用例:如果赋给volatile变量的值依靠于它的当前值(例如加法操作),如果你想操作是线程安全的,那就必须使用同步。
5. 原子字段更新器
当在多线程环境中加或减一个原始数据类型时,使用java.util.concurrent包中新添加的原子类会比编写你自己的同步代码块要好得多。原子类保证能以线程安全的方式来执行这些操作,如加减数值,更新值,以及添加值。原子类包括 AtomicInteger,AtomicBoolean,AtomicLong,AtomicLong等等。
使用原子类的挑战在于所有的类方法,包括get,set,以及get-set方法簇都是原子化的。这就意味着read和write操作不会以同步的方式来修改原子变量的值,也不仅仅重要的读-更新-写操作。如果你想对同步代码的发布能有更好的控制,解决方法就是使用原子字段更新器。
使用原子更新
原子字段更新器,如AtomicIntegerFieldUpdater,AtomicLongFieldUpdater和 AtomicReferenceFieldUpdater,是用于volatile字段的基本包装器类。在JDK的内部,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(Plain Old Java Object),只有一个字段: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类揭露了whatImReading属性就是你所期望的,该属性有get和set方法,但set方法做的一些事情不太一样。不同于简单地将内部的Book引用赋予一个特定的Book对象(使用清单3中被注释的代码就可以做到这一点),该示例使用了一个 AtomicReferenceFieldUpdater。
AtomicReferenceFieldUpdater
Javadoc对AtomicReferenceFieldUpdater有如下定义:
一个基于反射的工具类,它能对指定类的指定的volatile引用字段进行原子更新。该类被设计用于原子数据结构,在这种结构中,相同节点的多个引用字段会进行独立地原子更新。
在清单3中,通过调用AtomicReferenceFieldUpdater的静态方法newUpdater就能创建它的实例,该方法要接收三个参数:
包含该字段的对象的类(在这个例子中,就是MyObject)
将被自动更新的对象的类
将被自动更新的字段的名称
在执行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;
@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平台编写多线程应用时可能不知道的五件事情,包括同步方法与同步块的不同之处,使用ThreadLocal变量为每个线程去存储值,针对volatile关键字的广泛误解 (包括在需要同步时依赖volatile所产生的危险),还简要地看了一下原子类的复杂之处。查看资源以学习到更多相关知识。