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 7 程序设计.part2.rar(解压需2个文件part1,part2目前一次只能上传70M,不得不分卷,希望大家谅解下) 本书由全球资深Java技术专家、高级Java企业级应用架构师、《How Tomcat Works》作者亲自执笔,权威性...
高级java笔试题 《Java 并发编程实战》阅读笔记 ...因为并发、并行本身,是有悖于我们大脑的工作模式的,也就是说,我们长期的写码不得不 而一旦将这一个一个的线程组合起来,奇妙的 bug 发生了...
面试必考之HashMap源码分析与实现 ,微服务架构之Spring Cloud Eureka 场景分析与实战,高性能必学之Mysql主从架构实践 ,架构师不得不知道的Spring事物不能回滚的深层次原因 ,分库分表之后分布式下如何保证ID全局...
做为Java开发人员,这本书,可以说是不得不看的经典、进阶书籍!值得推荐
谈到并发,不得不谈ReentrantLock;而谈到ReentrantLock,不得不谈AbstractQueuedSynchronizer(AQS)! 类如其名,抽象的队列式的同步器,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它...
利用 Mina 可以高效地完成以下任务: <br>TCP/IP 和 UDP/IP 通讯 串口通讯 VM 间的管道通讯... 不过不管怎样,如果你在使用 Java 进行并发网络应用开发, Mina 绝对是一个值得推荐和学习的优秀工具!
ava中的关键字synchronized是一种用于实现线程同步的机制。它可以确保在同一时刻,只有一个线程...这意味着,如果有其他线程正在等待这个对象的锁,它们将不得不重新竞争这个锁。这种竞争可能导致锁膨胀现象的发生。
2017卧底面试题答案解析.txt 包含有 SpringMvc深入理解源码分析 高性能网络编程必备技能之IO与NIO阻塞分析 ...架构师不得不知道的Spring事物不能回滚的深层次原因 大型公司面试必答之数据结构与算法精讲 ... 等
不得不说一下,力扣网按专题总结的这些** 真的很好** ,大家可以多看看,融会贯通。 com.interview包下是递归和循环控制的实例 com.concurrency包为Java并发相关的程序,具体内容移植到springBoot项目,参见 ...
我就是当时没有打好基础,最后学习Java web、Java框架的时候对这些东西很难有深刻的理解,所以我不得不回头来恶补基础。今天我们就来谈谈并发那些事。 并发 并发就是将程序划分为多个任务,每个任务可以在同一时间内...
但是我不得不说服其他人,开发人员可以使用Java,并且Twisted(Python的事件驱动框架)阻碍了进步。 我编写了这种体系结构,以展示Java如何在项目中发挥作用。 简化的线程模型 由于一致担心,Java被拒绝了。 我花...
以至于正当Microsoft尽力在Visual J++基础上拓展Java功能,并使之与Windows操作系统紧密结合在一起的时候,Sun公司对Microsoft提出了法律诉讼,控告Microsoft违反了许可证协议中的条款,最终的结果是Microsoft公司...
A: 由于下载人数众多,下载服务器做了并发的限制。若发现下载不了,请稍后再试,多次下载是不会重复扣分的. Q: 我的积分不多了,如何获取积分? A: 上传优质资源可以获取积分,详细见积分规则。 下载本文件意味着您...
FAQ为什么我点的下载下不了,但积分却被扣了由于下载人数众多,下载服务器做了并发的限制。若发现下载不了,请稍后再试,多次下载是不会重复扣分的。我的积分不多了,如何获取积分?上传优质资源可以获取积分,详细见 ...
FAQ为什么我点的下载下不了,但积分却被扣了由于下载人数众多,下载服务器做了并发的限制。若发现下载不了,请稍后再试,多次下载是不会重复扣分的。我的积分不多了,如何获取积分?上传优质资源可以获取积分,详细见 ...
A: 由于下载人数众多,下载服务器做了并发的限制。若发现下载不了,请稍后再试,多次下载是不会重复扣分的. Q: 我的积分不多了,如何获取积分? A: 上传优质资源可以获取积分,详细见积分规则。 下载本文件意味着您...
这些收集器在某点上(比如压缩时)一般都不得不停止其他操作以完成特定的任务,但是因为其他应用程序可进行其他的后台操作,所以中断其他处理的实际时间大大降低。 2.7.并行收集器 并行收集器使用某种传统的算法并...