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 在访问易失性变量时确实隐式地提供同步,但是有一条重要提醒:读取易失性变量是同步的,写入易失性变量也是同步的,但非原子操作不同步。
这表示下面的代码不是线程安全的:
上一条语句也可写成:
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
满足同步化需求的危险),以及对原子类的错杂之处的一个简要介绍。参见 参考资料 部分了解更多内容。
相关推荐
Java 并发——基石篇 的 pdf 文档,原文章我发布在知乎上了: ...但是知乎的格式不太好看,另外有字数限制,我不得不将文档分为三个部分。 这里提供一份 pdf 格式的文档,格式比较好看,方便大家。
JAVA 7 程序设计.part1.rar(解压需2个文件part1,part2目前一次只能上传70M,不得不分卷,希望大家谅解下) 本书由全球资深Java技术专家、高级Java企业级应用架构师、《How Tomcat Works》作者亲自执笔,权威性...
架构师不得不知道的Spring事物不能回滚的深层次原因.mp4 │ │ │ ├─13.RPC底层通讯原理之Netty线程模型源码分析 │ │ 13.RPC底层通讯原理之Netty线程模型源码分析.wmv │ │ │ ├─14.分库分表之后分布式下...
这份压缩包文件"Java工程师不得不看的几千份代码"显然是一份丰富的资源集合,旨在帮助Java开发者拓宽视野,掌握更多实践技巧,提升编程能力。下面我们将深入探讨其中可能涵盖的关键知识点。 1. **基础语法与最佳...
提到并发,就不得不提Java内存模型(JMM)。JMM规定了线程之间的通信和可见性,确保了在并发环境下的正确性。volatile关键字是JMM的一部分,它可以确保变量的修改对其他线程立即可见,但不保证操作的原子性。而...
### Java虚拟机规范(Java SE 7)关键知识点解析 #### 一、概述 《Java虚拟机规范(Java SE 7版)》是一份详细介绍了Java虚拟机(JVM)架构和运行机制的重要文档。这份规范不仅是Java开发人员深入了解Java语言特性的...
JAVA 7 程序设计.part2.rar(解压需2个文件part1,part2目前一次只能上传70M,不得不分卷,希望大家谅解下) 本书由全球资深Java技术专家、高级Java企业级应用架构师、《How Tomcat Works》作者亲自执笔,权威性...
谈到Java集合类,不得不提的就是迭代器(Iterator)。迭代器是Java集合框架中非常重要的一个接口,它允许遍历集合中的元素。快速失败(Fail-Fast)迭代器机制是指在用迭代器遍历集合的过程中,如果检测到集合在迭代...
通过模拟真实的考试环境,测试涵盖Java SE 8的各个方面,包括基本的语法、面向对象的概念、核心API的使用、异常处理、数据结构、集合框架、流、Lambda表达式以及Java的并发机制等。这些练习测试有助于读者巩固知识点...
- 不得使用 Java 关键字作为标识符。 ### 9. 运算符 **知识点概述:** Java 提供了多种运算符,包括算术运算符、关系运算符、逻辑运算符和位运算符等。 **详细解释:** - **算术运算符:** 如加 (`+`)、减 (`-`)...
"java习题不得不看"这个资源正是一份专为新手设计的Java学习资料,通过一系列习题帮助初学者巩固和提升Java编程能力。 首先,我们要理解Java的基础概念。Java是一种面向对象的编程语言,由Sun Microsystems(现已被...
### Java并发知识点详解 #### 一、JDK发展与并发改进 - **JDK1.0**: 提供了首个纯解释执行的Java虚拟机(JVM)。此时并未涉及并发技术。 - **JDK1.3**: 引入了J2SE、J2EE和J2ME三个发展方向,同时JVM首次内置了...
在当今数字化时代,网上订餐系统已经成为了餐饮业不可或缺的一部分。这个名为“java网上订餐系统前后台.rar”的压缩包,包含了用Java语言开发的一个完整的网上订餐系统的源代码,旨在提供学习和参考的机会,而非用于...
高级java笔试题 《Java 并发编程实战》阅读笔记 ...因为并发、并行本身,是有悖于我们大脑的工作模式的,也就是说,我们长期的写码不得不 而一旦将这一个一个的线程组合起来,奇妙的 bug 发生了...
面试必考之HashMap源码分析与实现 ,微服务架构之Spring Cloud Eureka 场景分析与实战,高性能必学之Mysql主从架构实践 ,架构师不得不知道的Spring事物不能回滚的深层次原因 ,分库分表之后分布式下如何保证ID全局...
阿里Java开发规范是一套详尽的编程规范,由阿里巴巴集团技术部于2016年12月首次公开,旨在指导Java开发人员编写高质量、易维护、可读性强的代码。以下是从给定文件中提取的知识点。 命名规约: 1. 编程相关命名不...
### 阿里巴巴Java开发手册(华山版)知识点概览 #### 一、编程规约 **1. 命名风格** - **强制规定**:代码中的命名不应以下划线`_`或美元符号`$`开始或结束。 - **反例**:_name, __name, $name, name_, name$, ...