您不知道的多线程编程 多线程编程原理
yuyutoo 2024-10-12 01:02 8 浏览 0 评论
有关高性能线程处理的微妙之处
虽然很少有 Java? 开发人员能够忽视多线程编程和支持它的 Java 平台库,更少有人有时间深入研究线程。相反地,我们临时学习线程,在需要时向我们的工具箱添加新的技巧和技术。以这种方式构建和运行适当的应用程序是可行的,但是您可以做的不止这些。理解 Java 编译器的线程处理特性和 JVM 将有助于您编写更高效、性能更好的 Java 代码。
在这期的 5 件事 系列 中,我将通过同步方法、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 变量
如果您想为一个类的所有实例维持一个变量的实例,将会用到静态类成员变量。如果您想以线程为单位维持一个变量的实例,将会用到线程局部变量。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++;
上一条语句也可写成:
int temp = 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 类
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方法所做的有点不同。它不仅仅将其内部 Book 引用分配给指定的 Book(这将使用 清单 3 中注释出的代码来完成),而是使用一个 AtomicReferenceFieldUpdater。
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 平台上编写多线程应用程序您可能不知道的 5 件事,包括同步化方法与同步化代码块之间的不同,为每个线程存储运用 ThreadLocal 变量的价值,被广泛误解的 volatile 关键字(包括依赖于 volatile 满足同步化需求的危险),以及对原子类的错杂之处的一个简要介绍。参见 参考资料 部分了解更多内容。
相关推荐
- 墨尔本一华裔男子与亚裔男子分别失踪数日 警方寻人
-
中新网5月15日电据澳洲新快网报道,据澳大利亚维州警察局网站消息,22岁的华裔男子邓跃(Yue‘Peter’Deng,音译)失踪已6天,维州警方于当地时间13日发布寻人通告,寻求公众协助寻找邓跃。华...
- 网络交友须谨慎!美国犹他州一男子因涉嫌杀害女网友被捕
-
伊森·洪克斯克(图源网络,侵删)据美国广播公司(ABC)25日报道,美国犹他州一名男子于24日因涉嫌谋杀被捕。警方表示,这名男子主动告知警局,称其杀害了一名在网络交友软件上认识的25岁女子。雷顿警...
- 一课译词:来龙去脉(来龙去脉 的意思解释)
-
Mountainranges[Photo/SIPA]“来龙去脉”,汉语成语,本指山脉的走势和去向,现比喻一件事的前因后果(causeandeffectofanevent),可以翻译为“i...
- 高考重要考点:range(range高考用法)
-
range可以用作动词,也可以用作名词,含义特别多,在阅读理解中出现的频率很高,还经常作为完形填空的选项,而且在作文中使用是非常好的高级词汇。...
- C++20 Ranges:现代范围操作(现代c++白皮书)
-
1.引言:C++20Ranges库简介C++20引入的Ranges库是C++标准库的重要更新,旨在提供更现代化、表达力更强的方式来处理数据序列(范围,range)。Ranges库基于...
- 学习VBA,报表做到飞 第二章 数组 2.4 Filter函数
-
第二章数组2.4Filter函数Filter函数功能与autofilter函数类似,它对一个一维数组进行筛选,返回一个从0开始的数组。...
- VBA学习笔记:数组:数组相关函数—Split,Join
-
Split拆分字符串函数,语法Split(expression,字符,Limit,compare),第1参数为必写,后面3个参数都是可选项。Expression为需要拆分的数据,“字符”就是以哪个字...
- VBA如何自定义序列,学会这些方法,让你工作更轻松
-
No.1在Excel中,自定义序列是一种快速填表机制,如何有效地利用这个方法,可以大大增加工作效率。通常在操作工作表的时候,可能会输入一些很有序的序列,如果一一录入就显得十分笨拙。Excel给出了一种...
- Excel VBA入门教程1.3 数组基础(vba数组详解)
-
1.3数组使用数组和对象时,也要声明,这里说下数组的声明:'确定范围的数组,可以存储b-a+1个数,a、b为整数Dim数组名称(aTob)As数据类型Dimarr...
- 远程网络调试工具百宝箱-MobaXterm
-
MobaXterm是一个功能强大的远程网络工具百宝箱,它将所有重要的远程网络工具(SSH、Telnet、X11、RDP、VNC、FTP、MOSH、Serial等)和Unix命令(bash、ls、cat...
- AREX:携程新一代自动化回归测试工具的设计与实现
-
一、背景随着携程机票BU业务规模的不断提高,业务系统日趋复杂,各种问题和挑战也随之而来。对于研发测试团队,面临着各种效能困境,包括业务复杂度高、数据构造工作量大、回归测试全量回归、沟通成本高、测试用例...
- Windows、Android、IOS、Web自动化工具选择策略
-
Windows平台中应用UI自动化测试解决方案AutoIT是开源工具,该工具识别windows的标准控件效果不错,但是当它遇到应用中非标准控件定义的UI元素时往往就无能为力了,这个时候选择silkte...
- python自动化工具:pywinauto(python快速上手 自动化)
-
简介Pywinauto是完全由Python构建的一个模块,可以用于自动化Windows上的GUI应用程序。同时,它支持鼠标、键盘操作,在元素控件树较复杂的界面,可以辅助我们完成自动化操作。我在...
- 时下最火的 Airtest 如何测试手机 APP?
-
引言Airtest是网易出品的一款基于图像识别的自动化测试工具,主要应用在手机APP和游戏的测试。一旦使用了这个工具进行APP的自动化,你就会发现自动化测试原来是如此简单!!连接手机要进行...
- 【推荐】7个最强Appium替代工具,移动App自动化测试必备!
-
在移动应用开发日益火爆的今天,自动化测试成为了确保应用质量和用户体验的关键环节。Appium作为一款广泛应用的移动应用自动化测试工具,为测试人员所熟知。然而,在不同的测试场景和需求下,还有许多其他优...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- mybatis plus (70)
- scheduledtask (71)
- css滚动条 (60)
- java学生成绩管理系统 (59)
- 结构体数组 (69)
- databasemetadata (64)
- javastatic (68)
- jsp实用教程 (53)
- fontawesome (57)
- widget开发 (57)
- vb net教程 (62)
- hibernate 教程 (63)
- case语句 (57)
- svn连接 (74)
- directoryindex (69)
- session timeout (58)
- textbox换行 (67)
- extension_dir (64)
- linearlayout (58)
- vba高级教程 (75)
- iframe用法 (58)
- sqlparameter (59)
- trim函数 (59)
- flex布局 (63)
- contextloaderlistener (56)