`
fkbillgates
  • 浏览: 42507 次
  • 性别: Icon_minigender_1
  • 来自: 大连
文章分类
社区版块
存档分类
最新评论

java多线程中的5个秘密

阅读更多

虽然很少有 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 包中找到的其中一个新原子类比编写自己的同步代码块要好得多。原子类确保某些操作以线程安全方式被执行,比如递增和递减一个值,更新一个值,添加一个值。原子类列表包括 AtomicIntegerAtomicBooleanAtomicLongAtomicIntegerArray 等等。

使用原子类的难题在于,所有类操作,包括 getset 和一系列 get-set 操作是以原子态呈现的。这表示,不修改原子变量值的 readwrite 操作是同步的,不仅仅是重要的 read-update-write 操作。如果您希望对同步代码的部署进行更多细粒度控制,那么解决方案就是使用一个原子字段更新程序。

使用原子更新

AtomicIntegerFieldUpdaterAtomicLongFieldUpdaterAtomicReferenceFieldUpdater 之类的原子字段更新程序基本上是应用于易失性字段的封装器。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 类通过 getset 方法公开其 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
6
分享到:
评论

相关推荐

    基于Java多线程隐藏数组下标变换表达式的代码迷惑算法.pdf

    在本文中,我们提出了一种基于Java多线程的代码迷惑算法,该算法可以隐藏数组下标变换表达式,以防止源代码静态分析和基于源代码植入反迷惑攻击。该算法的实现基于Java多线程编程模型,可以充分利用多核处理器的计算...

    Java跨平台得秘密.pdf

    本文档详细介绍了Java跨平台的秘密,包括Java虚拟机、Java Runtime Environment、Java Development Kit、Java基本数据类型、Java操作符、Java控制流、Java函数、Java继承、Java接口、Java多线程、Java字符串和Java...

    java 培训机构秘密课件 很给力0

    7. **多线程**:Java内置对多线程的支持,讲解如何创建和管理线程,以及同步机制如synchronized关键字和Lock接口。 8. **Java Swing或JavaFX**:这两者是Java的图形用户界面(GUI)库,用于创建桌面应用程序。 9. ...

    3306多线程爆破1.2.zip

    攻击者通常会编写一个工具,例如压缩包中的`mysql1.2.jar`,这是一个Java可执行文件,可能包含了实现多线程爆破的代码。它会使用预定义的密码字典(这可能是从网上获取的常见密码列表或者根据特定策略生成的)去尝试...

    Java讲义 Java资料

    - **多线程**:Java内置了多线程支持,可以通过继承Thread类或实现Runnable接口来创建多线程程序。多线程机制允许程序并行执行,提高程序效率,并且通过同步机制保证了对共享数据的正确操作。 - **可移植性**:Java...

    JVM参数调优指南:解锁Java性能优化的秘密

    4. **多线程**:Java内置了对多线程编程的支持,允许开发者创建同时执行的多个线程。 5. **网络编程**:Java提供了丰富的网络通信API,使得开发网络应用变得容易。 6. **安全性**:Java提供了一个安全

    【Java面试+Java学习指南】 一份涵盖大部分Java程序员所需要掌握的核心知识

    多线程 深入理解内部类 javac和javap Java8新特性终极指南 序列化和反序列化 继承、封装、多态的实现原理 容器 Java集合类总结 Java集合详解1:一文读懂ArrayList,Vector与Stack使用方法和实现原理 Java集合详解2:...

    Java线程/内存模型的缺陷和增强

    它提供了Thread/Runnable/ThreadGroup等一系列封装的类和接口,让程序员可以高效的开发Java多线程应用。为了实现同步,Java提供了synchronize关键字以及object的wait()/notify()机制,可是在简单易用的背后,应藏着...

    Java工程师面试复习指南

    多线程 深入理解内部类 javac和javap Java8新特性终极指南 序列化和反序列化 继承封装多态的实现原理 集合类 Java集合类总结 Java集合详解:一文读懂ArrayList,Vector与Stack使用方法和实现原理 Java集合详解:Queue...

    JAVA 特殊效果案例

    本篇文章将深入探讨JAVA实现的五个特殊效果案例:彩蛋、画图、身份证转换、时钟以及字体线程。这些案例不仅展示了JAVA在图形用户界面(GUI)开发中的潜力,还体现了其在处理数据和时间操作上的灵活性。 1. 彩蛋...

    武汉大学JAVA教程

    - 线程:支持多线程编程,实现并发执行任务。 - 集合操作:如ArrayList、LinkedList、HashSet等,提供数据存储和操作的接口。 - 输入/输出:使用IOException和相关类进行文件读写和网络通信。 - 网络编程:...

    JAVA项目暗箱和改写项目的图片

    这涉及两种语言特性的理解和互换,例如,Java的垃圾回收机制与C++的内存管理,Java的多线程模型与C++的线程库等。改写过程中需考虑兼容性、性能优化和代码复用。 5. **资源扩展**:在项目改写过程中,资源扩展可能...

    JAVA面试题2019

    - 是否线程安全:**HashMap** 在多线程环境下不保证线程安全,因为其内部操作不是原子性的。 - 扩容过程:当元素数量超过容量和负载因子计算出的阈值时触发,涉及到重新散列。 - **1.7与1.8的区别**:1.8引入了树...

    JAVA内存模型

    `synchronized`关键字通过上述过程保证了多线程环境下的数据一致性。如果没有使用`synchronized`,则主内存和线程的工作内存之间的数据交换是非严格同步的,可能会导致数据的不一致性和可见性问题。 #### 五、...

    Java资源.docx

    9. **多线程**:Java内置了对多线程的支持,可以轻松实现并发处理。 10. **动态性**:Java语言设计灵活,支持动态加载类和方法。 #### 发展历程 - **1995年**:Sun Microsystems正式发布了Java语言的第一个版本...

    Java 编程从零到精通的综合训练营.zip

    Java 编程从零到精通的综合训练营课程涵盖的主题第 1 部分让我们来了解 Java第 2 节Java 中的原始数据类型第 3 节Java 程序执行时幕后发生了什么第 4 部分深入研究 Java 类、方法、字段、构造函数第 5 节Java 中的...

    aws-sqs-thread-examples

    MultiThreadQueueReceiver.java 在多线程中从队列中取出消息。 需要 Java8 AWS 账户 用法 获取代码 $ git clone https://github.com/uzresk/aws-sqs-thead-examples.git 创建队列 1.创建一个名为“MyQueue”的...

    java讲义(入门基础)

    - Java内置了Thread类,开发者可以通过继承该类轻松编写多线程程序。 - 多线程机制允许应用程序并行执行,同步机制确保了对共享数据的正确访问。 - 多线程有助于实现实时交互行为。 5. **垃圾收集**: - Java...

    Java写的俄罗斯方块附源码

    从源码中学习,开发者可以理解如何将这些概念实际应用到Swing程序中,同时也能了解到事件处理、图形绘制、线程同步(因为游戏需要实时更新)等Java编程技巧。此外,源码还可以作为一个很好的学习资源,帮助初学者...

Global site tag (gtag.js) - Google Analytics