尽管很少有Java™开发人员能够忽略多线程编程和支持它的Java平台库,但更少的人有时间深入研究线程。 相反,我们是专门学习线程的,并在需要时向工具箱添加了新的技巧和技术。 可以通过这种方式构建和运行良好的应用程序,但是您可以做得更好。 了解Java编译器和JVM的线程特性将有助于您编写更高效,性能更好的Java代码。
在这5篇系列文章的本期中,我将介绍具有同步方法,易失性变量和原子类的多线程编程的一些微妙方面。 我的讨论特别关注这些构造中的一些如何与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变量
如果要为一个类的所有实例维护一个变量的单个实例,则将使用静态类成员变量来执行此操作。 如果要基于每个线程维护变量的实例,则将使用线程局部变量。 ThreadLocal
变量与普通变量的不同之处在于,每个线程都有自己单独的变量实例,可以通过get()
或set()
方法访问该实例。
假设您正在开发一个多线程代码跟踪器,其目标是唯一地标识代码中每个线程的路径。 挑战在于您需要在多个线程之间协调多个类中的多个方法。 没有ThreadLocal
,这将是一个复杂的问题。 当线程开始执行时,它将需要生成一个唯一的令牌以在跟踪器中对其进行标识,然后将该唯一的令牌传递给跟踪中的每个方法。
使用ThreadLocal
,事情变得更简单。 线程在执行开始时初始化线程局部变量,然后从每个类中的每个方法访问它,并确保该变量将仅承载当前执行线程的跟踪信息。 执行完毕后,线程可以将其特定于线程的跟踪传递给负责维护所有跟踪的管理对象。
当您需要基于每个线程存储变量实例时,使用ThreadLocal
很有意义。
3.易变变量
我估计大约有一半的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的支持来自动同步易失性变量。 更好的方法取决于您的用例:如果volatile变量的赋值取决于它的当前值(例如在增量操作期间),那么如果您希望该操作是线程安全的,则必须使用同步。
5.原子场更新器
在多线程环境中增加或减少基本类型时,与编写自己的同步代码块相比,使用java.util.concurrent.atomic
包中的原子类之一要好得多。 原子类保证某些操作将以线程安全的方式执行,例如递增和递减值,更新值以及添加值。 原子类的列表包括AtomicInteger
, AtomicBoolean
, AtomicLong
, AtomicIntegerArray
等等。 原子包的最新添加是DoubleAccumulator
, DoubleAdder
, LongAccumulator
和LongAdder
类。 它们维护一组内部变量,以减少争用并围绕给定的lambda表达式进行操作。
使用原子类的挑战在于,所有类操作(包括get
, set
和get-set
操作族)都被呈现为原子。 这意味着不修改原子变量值的read
和write
操作将同步,而不仅仅是重要的read-update-write
操作。 如果要对同步代码的部署进行更细粒度的控制,则解决方法是使用原子字段更新程序。
使用原子更新
诸如AtomicIntegerFieldUpdater
, AtomicLongFieldUpdater
和AtomicReferenceFieldUpdater
类的原子字段更新器基本上是应用于易失性字段的包装器。 在内部,Java类库使用它们。 尽管它们未在应用程序代码中广泛使用,但没有理由也不能使用它们。
清单2给出了一个使用原子更新来更改某人正在阅读的书的类的示例:
清单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。
清单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
方法的作用有些不同。 它使用AtomicReferenceFieldUpdater
而不是简单地将其内部Book
引用分配给指定的Book
(可以使用清单3中注释的代码来完成)。
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;
@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
满足同步需求的危险),并简要介绍一下原子类的复杂性。
翻译自: https://www.ibm.com/developerworks/java/library/j-5things15/index.html