举例解析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 满足同步化需求的危险),以及对原子类的错杂之处的一个简要介绍。参见 参考资料 部分了解更多内容。

时间: 2015-11-21

Java多线程的实现方式比较(两种方式比较)

先看一下java线程运行时各个阶段的运行状态 线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源.一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行.由于线程之间的相互制约,致使线程在运行中呈现出间断性. 在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位.由于线程比进程更小,基本上不拥有系统资源,故对它的

java多线程抓取铃声多多官网的铃声数据

一直想练习下java多线程抓取数据. 有天被我发现,铃声多多的官网(http://www.shoujiduoduo.com/main/)有大量的数据. 通过观察他们前端获取铃声数据的ajax http://www.shoujiduoduo.com/ringweb/ringweb.php?type=getlist&listid={类别ID}&page={分页页码} 很容易就能发现通过改变 listId和page就能从服务器获取铃声的json数据, 通过解析json数据, 可以看到都带有{&q

java多线程编程制作电子时钟

模拟一个电子时钟,它可以在任何时候被启动或者停止,并可以独立的运行. 1.定义一个Clock类.它继承Label类,并实现Runnable接口.这个类中有一个Thread类型的clocker域,以及start()和run()方法.在run()方法中,每隔一秒就把系统时间显示为label的文本. class Clock extends Label implements Runnable { //定义Thread类型的clocker域 public Thread clocker=null; publ

java多线程编程实现下雪效果

没有直接采用继承Thread类或者继承Runnable的接口来实现多线程,而是使用了匿名内部类. 要导入的类: import javax.swing.*; import java.awt.*; 1.定义SowPanel类,继承JPanel类,这个类有两个整型数组成员,用来保存雪花起始位置.在构造函数中为数组赋初值:重写父类的paint()方法:定义一个启动多线程的startSnow()方法. class SnowPanel extends JPanel { //定义整型数组,存储雪花坐标 pri

java多线程编程之Synchronized块同步方法

文章分享了4个例子对synchronized的详细解释 1.是否加synchronized关键字的不同 public class ThreadTest { public static void main(String[] args) { Example example = new Example(); Thread t1 = new Thread1(example); Thread t2 = new Thread1(example); t1.start(); t2.start(); } } cl

java多线程编程之Synchronized关键字详解

本文介绍JAVA多线程中的synchronized关键字作为对象锁的一些知识点. 所谓对象锁,就是就是synchronized 给某个对象 加锁.关于 对象锁 可参考:这篇文章  一.分析 synchronized可以修饰实例方法,如下形式: public class MyObject { synchronized public void methodA() { //do something.... } 这里,synchronized 关键字锁住的是当前对象.这也是称为对象锁的原因. 为啥锁住当

java多线程编程之java线程简介

一.线程概述 线程是程序运行的基本执行单元.当操作系统(不包括单线程的操作系统,如微软早期的DOS)在执行一个程序时,会在系统中建立一个进程,而在这个进程中,必须至少建立一个线程(这个线程被称为主线程)来作为这个程序运行的入口点.因此,在操作系统中运行的任何程序都至少有一个主线程.进程和线程是现代操作系统中两个必不可少的运行模型.在操作系统中可以有多个进程,这些进程包括系统进程(由操作系统内部建立的进程)和用户进程(由用户程序建立的进程):一个进程中可以有一个或多个线程.进程和进程之间不共享内存

Java多线程编程之Lock用法实例

锁是控制多个线程对共享资源进行访问的工具.通常,锁提供了对共享资源的独占访问.一次只能有一个线程获得锁,对共享资源的所有访问都需要首先获得锁.不过,某些锁可能允许对共享资源并发访问,如 ReadWriteLock(维护了一对相关的锁,一个用于只读操作,另一个用于写入操作) 的读写锁. 1.Lock提供了无条件的.可轮询的.定时的.可中断的锁获取操作,所有加锁和解锁的方法都是显式的. public interface Lock{ void lock(); //加锁 //优先考虑响应中断,而不是响应

Java多线程编程之ThreadLocal线程范围内的共享变量

模拟ThreadLocal类实现:线程范围内的共享变量,每个线程只能访问他自己的,不能访问别的线程. package com.ljq.test.thread; import java.util.HashMap; import java.util.Map; import java.util.Random; /** * 线程范围内的共享变量 * * 三个模块共享数据,主线程模块和AB模块 * * @author Administrator * */ public class ThreadScopeS

java多线程编程之join方法的使用示例

在上面的例子中多次使用到了Thread类的join方法.我想大家可能已经猜出来join方法的功能是什么了.对,join方法的功能就是使异步执行的线程变成同步执行.也就是说,当调用线程实例的start方法后,这个方法会立即返回,如果在调用start方法后后需要使用一个由这个线程计算得到的值,就必须使用join方法.如果不使用join方法,就不能保证当执行到start方法后面的某条语句时,这个线程一定会执行完.而使用join方法后,直到这个线程退出,程序才会往下执行.下面的代码演示了join的用法.

Java多线程编程之CountDownLatch同步工具使用实例

好像倒计时计数器,调用CountDownLatch对象的countDown方法就将计数器减1,当到达0时,所有等待者就开始执行. java.util.concurrent.CountDownLatch 一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待.用给定的计数初始化CountDownLatch.由于调用了countDown()方法,所以在当前计数到达零之前,await方法会一直受阻塞.之后,会释放所有等待的线程,await的所有后续调用都将立即返回.这种现

java多线程编程之InheritableThreadLocal

InheritableThreadLocal的作用: 当我们需要在子线程中使用父线程中的值得时候我们就可以像使用ThreadLocal那样来使用InheritableThreadLocal了. 首先我们来看一下InheritableThreadLocal的jdk源码: package java.lang; import java.lang.ref.*; public class InheritableThreadLocal<T> extends ThreadLocal<T> { p

解析Java编程之Synchronized锁住的对象

图片上传 密码修改为  synchronized是java中用于同步的关键字,一般我们通过Synchronized锁住一个对象,来进行线程同步.我们需要了解在程序执行过程中,synchronized锁住的到底是哪个对象,否则我们在多线程的程序就有可能出现问题. 看下面的代码,我们定义了一个静态变量n,在run方法中,我们使n增加10,然后在main方法中,我们开辟了100个线程,来执行n增加的操作,如果线程没有并发执行,那么n最后的值应该为1000,显然下面的程序执行完结果不是1000,因为我们

java并发编程之cas详解

CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术.简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替换当前变量的值.这听起来可能有一点复杂但是实际上你理解之后发现很简单,接下来,让我们跟深入的了解一下这项技术. CAS的使用场景 在程序和算法中一个经常出现的模式就是"check and act"模式.先检查后操作模式发生在代码中首先检查一个变量的值,然后再基于这个值做一些操作.下面是一个