举例解析Java多线程编程中需要注意的一些关键点
1.同步方法或同步代码块?
您可能偶尔会思考是否要同步化这个方法调用,还是只同步化该方法的线程安全子集。在这些情况下,知道Java编译器何时将源代码转化为字节代码会很有用,它处理同步方法和同步代码块的方式完全不同。
当JVM执行一个同步方法时,执行中的线程识别该方法的method_info结构是否有ACC_SYNCHRONIZED标记设置,然后它自动获取对象的锁,调用方法,最后释放锁。如果有异常发生,线程自动释放锁。
另一方面,同步化一个方法块会越过JVM对获取对象锁和异常处理的内置支持,要求以字节代码显式写入功能。如果您使用同步方法读取一个方法的字节代码,就会看到有十几个额外的操作用于管理这个功能。清单1展示用于生成同步方法和同步代码块的调用:
清单1.两种同步化方法
packagecom.geekcap; publicclassSynchronizationExample{ privateinti; publicsynchronizedintsynchronizedMethodGet(){ returni; } publicintsynchronizedBlockGet(){ synchronized(this){ returni; } } }
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.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++;
上一条语句也可写成:
inttemp=0; synchronize(myVolatileVar){ temp=myVolatileVar; } temp++; synchronize(myVolatileVar){ myVolatileVar=temp; }
换言之,如果一个易失性变量得到更新,这样其值就会在底层被读取、修改并分配一个新值,结果将是一个在两个同步操作之间执行的非线程安全操作。然后您可以决定是使用同步化还是依赖于JRE的支持来自动同步易失性变量。更好的方法取决于您的用例:如果分配给易失性变量的值取决于当前值(比如在一个递增操作期间),要想该操作是线程安全的,那么您必须使用同步化。
5.原子字段更新程序
在一个多线程环境中递增或递减一个原语类型时,使用在java.util.concurrent.atomic包中找到的其中一个新原子类比编写自己的同步代码块要好得多。原子类确保某些操作以线程安全方式被执行,比如递增和递减一个值,更新一个值,添加一个值。原子类列表包括AtomicInteger、AtomicBoolean、AtomicLong、AtomicIntegerArray等等。
使用原子类的难题在于,所有类操作,包括get、set和一系列get-set操作是以原子态呈现的。这表示,不修改原子变量值的read和write操作是同步的,不仅仅是重要的read-update-write操作。如果您希望对同步代码的部署进行更多细粒度控制,那么解决方案就是使用一个原子字段更新程序。
使用原子更新
像AtomicIntegerFieldUpdater、AtomicLongFieldUpdater和AtomicReferenceFieldUpdater之类的原子字段更新程序基本上是应用于易失性字段的封装器。Java类库在内部使用它们。虽然它们没有在应用程序代码中得到广泛使用,但是也没有不能使用它们的理由。
清单2展示一个有关类的示例,该类使用原子更新来更改某人正在读取的书目:
清单2.Book类
packagecom.geeckap.atomicexample; publicclassBook { privateStringname; publicBook() { } publicBook(Stringname) { this.name=name; } publicStringgetName() { returnname; } publicvoidsetName(Stringname) { this.name=name; } }
Book类仅是一个POJO(Java原生类对象),拥有一个单一字段:name。
清单3.MyObject类
packagecom.geeckap.atomicexample; importjava.util.concurrent.atomic.AtomicReferenceFieldUpdater; /** * *@authorshaines */ publicclassMyObject { privatevolatileBookwhatImReading; privatestaticfinalAtomicReferenceFieldUpdater<MyObject,Book>updater= AtomicReferenceFieldUpdater.newUpdater( MyObject.class,Book.class,"whatImReading"); publicBookgetWhatImReading() { returnwhatImReading; } publicvoidsetWhatImReading(BookwhatImReading) { //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.演习原子更新的测试用例
packagecom.geeckap.atomicexample; importorg.junit.Assert; importorg.junit.Before; importorg.junit.Test; publicclassAtomicExampleTest { privateMyObjectobj; @Before publicvoidsetUp() { obj=newMyObject(); obj.setWhatImReading(newBook("Java2FromScratch")); } @Test publicvoidtestUpdate() { obj.setWhatImReading(newBook( "ProJavaEE5PerformanceManagementandOptimization")); Assert.assertEquals("Incorrectbookname", "ProJavaEE5PerformanceManagementandOptimization", obj.getWhatImReading().getName()); } }
结束语
多线程编程永远充满了挑战,但是随着Java平台的演变,它获得了简化一些多线程编程任务的支持。在本文中,我讨论了关于在Java平台上编写多线程应用程序您可能不知道的5件事,包括同步化方法与同步化代码块之间的不同,为每个线程存储运用ThreadLocal变量的价值,被广泛误解的volatile关键字(包括依赖于volatile满足同步化需求的危险),以及对原子类的错杂之处的一个简要介绍。参见参考资料部分了解更多内容。