本文节选自 Effective Java by Joshua Bloch 和 Concurrent Programming in Java by Doug Lea.

1.3 原子数据的同步

java语言保证读或写一个变量是原子(atomic)的,除非这个变量的类型是long或double.换句话说,读入一个非long或double类型的变量,可以保证返回值一定是某个线程保存在该变量中的,即使多个线程在没有同步的时候并发地修改这个变量,也是如此。   
    虽然原子性保证了一个线程在读写数据的时候,不会看到一个随机的数值,但是它并不保证一个线程写入的值对于另外一个线程是可见的。java的内存模型决定,为了在线程之间可靠地通信,以及为了互斥访问,对原子数据的读写进行同步是需要的。考虑下边的序列号生成程序:

  1. private static int nextSerialNumber = 0;
  2. public static int generateSerialNumber()
  3. {
  4. return nextSerialNumber++;
  5. }

这个程序的意图是保证每次调用generateSerialNumber都会返回一个不同的序列号,然而,如果没有同步,这个方法并不能正确的工作。递增操作符(++)既要读nextSerialNumber域,也要写nextSerialNumber域,所以它不是原子的。读和写相互独立的操作。因此,多个并发的线程可能会看到nextSerialNumber有相同的值,因而返回相同的序列号。此外,一个线程重复地调用generateSerialNumber,获得从0到n的一系列序列号之后,另外一个线程调用generateSerialNumber并获得一个序列号是0,这是有可能发生的。如果没有同步机制,第二个线程可能根本看不到第一个线程所作的改变。

1.4 监控机制
    正如每个Object都有一个锁, 每个Object也有一个等待集合(wait set),它有wait、notify、notifyAll和Thread.interrupt方法来操作。同时拥有锁和等待集合的实体,通常被成为监视器(monitor)。每个Object的等待集合是由JVM维护的。等待集合一直存放着那些因为调用对象的wait方法而被阻塞的线程。由于等待集合和锁之间的交互机制,只有获得目标对象的同步锁时,才可以调用它的wait、notify和notifyAll方法。这种要求通常无法靠编译来检查,如果条件不能满足,那么在运行的时候调用以上方法就会导致其抛出IllegalMonitorStateException。

wait 方法被调用后,会执行如下操作

  • 如果当前线程已经被中断,那么该方法立刻退出,然后抛出一个InterruptedException异常。否则线程会被阻塞。
  • JVM把该线程放入目标对象内部且无法访问的等待集合中。
  • 目标对象的同步锁被释放,但是这个线程锁拥有的其他锁依然会被这个线程保留着。当线程重新恢复质执行时,它会重新获得目标对象的同步锁

notify方法被调用后,会执行如下操作

  • 如果存在的话,JVM会从目标对象内部的等待集合中任意移除一个线程T。如果等待集合中的线程数大于1,那么哪个线程被选中完全是随机的。
  • T必须重新获得目标对象的同步锁,这必然导致它将会被阻塞到调用Thead.notify的线程释放该同步锁。如果其他线程在T获得此锁之前就获得它,那么T就要一直被阻塞下去。
  • T从执行wait的那点恢复执行。

notifyAll方法被调用后的操作和notify类似,不同的只是等待集合中所有的线程(同时)都要执行那些操作。然而等待集合中的线程必须要在竞争到目标对象的同步锁之后,才能继续执行。

interrupt。如果对一个因为调用了wait方法而被挂起的对象调用Thread.interrupt方法,那么这个方法的执行机制就和notify类似,只是在重新获得对象锁后,该方法就会抛出InterruptedException异常,并且该线程的中断状态被置为false。

对于Object.wait()方法,它一定是在一个同步区域中被调用,而且该同步区域锁住了被调用的对象。下边是使用Object.wait()方法的标准模式:

总是要使用wait循环模式来调用wait方法,永远不要在循环的外边调用wait方法。循环的作用在于在等待的前、后都能测试条件。在等待之前测试条件,如果条件成立的话则跳过等待,这对于确保程序的活性(liveness)是必要的。如果条件已经成立,而且在线程等待之前notify(或者notifyAll)方法已经被调用过,那么无法保证该线程将总会从等待中醒过来。在等待之后测试条件,如果条件不成立的话则继续等待,这对于确保程序的安全性(safety)是必要的。当条件不成立的时候,如果线程继续执行,那么可能破坏被锁保护的约束关系。当条件不成立的时候,有以下一些理由可以使一个线程醒过来:

  1. 从一个线程调用notify方法的时刻起,到等待线程被唤醒的时刻之间,另一个线程得到了锁,并且改变了被保护的状态。
  2. 条件没有成立,但是另外一个线程可能意外或者恶意地调用了notify方法。在公有对象上调用wait方法,这其实是将自己暴露在危险的境地中。因为任何持有这个对象引用的线程都可以调用该对象的notify方法。
  3. 在没有被通知的情况下等待线程也可能被唤醒。这被称为“伪唤醒(spurious wakeup)”。虽然《Java语言规范(The Java Language Specification )》并没有提到这种可能,但是许多JVM实现都使用了具有伪唤醒功能的线程设施,尽管用的很少。

与此相关的一个问题是,为了唤醒正在等待的线程,到底应该使用notify方法还是应该使用notifyAll方法。假设所有的wait调用都是在循环的内部,那么使用notifyAll方法是一个合理而保守的做法。它总会产生正确的结果,它可以保证会唤醒所有需要被唤醒的线程。当然,这样也会唤醒其它一些线程,但是这不会影响程序的正确性。这些线程醒来之后会检查等待条件,发现条件不满足,就会继续等待。使用notifyAll方法的另外一个优点在于可以避免来自不相关线程的意外或者恶意等待。否则的话,这样的等待可能会“吞掉”一个关键的通知,使真正的接收线程无限地等待下去。关于使用notifyAll方法的一个不足在于,虽然使用notifyAll方法不会影响程序的正确性,但是会影响程序的性能。

1.5 死锁
    尽管完全同步的原子操作很安全,但是线程可能却因此失去了活性(liveness)。死锁(dead lock)是在两个或多个线程都有权限访问两个或多个对象,并且每个线程都在已经得到一个锁的情况下等待其它线程已经得到的锁。假设线程A持有的对象X的锁,并且正在试图获得对象Y的锁,同时,线程B已经拥有的对象Y的锁,并在试图获得对象X的锁。因此没有哪个线程能够执行进一步的操作,死锁就产生了。例如:

  1. public class Cell {
  2. private long value;
  3. public Cell(long value) {
  4. this.value = value;
  5. }
  6. public synchronized long getValue() {
  7. return value;
  8. }
  9. public synchronized void setValue(long value) {
  10. this.value = value;
  11. }
  12. public synchronized void swap(Cell other) {
  13. long t = getValue();
  14. long v = other.getValue();
  15. setValue(v);
  16. other.setValue(t);
  17. }
  18. public static void main(String args[]) throws Exception {
  19. //
  20. final Cell c1 = new Cell(100);
  21. final Cell c2 = new Cell(200);
  22. //
  23. Thread t1 = new Thread(new Runnable() {
  24. public void run() {
  25. long count = 0;
  26. try {
  27. while(true) {
  28. c1.swap(c2);
  29. count++;
  30. if(count % 100 == 0) {
  31. System.out.println("thread1's current progress: " + count);
  32. }
  33. }
  34. } catch (Exception e) {
  35. e.printStackTrace();
  36. }
  37. }
  38. });
  39. t1.setName("thread1");
  40. //
  41. Thread t2 = new Thread(new Runnable() {
  42. public void run() {
  43. long count = 0;
  44. try {
  45. while(true) {
  46. c2.swap(c1);
  47. count++;
  48. if(count % 100 == 0) {
  49. System.out.println("thread2's current progress: " + count);
  50. }
  51. }
  52. } catch (Exception e) {
  53. e.printStackTrace();
  54. }
  55. }
  56. });
  57. t2.setName("thread2");
  58. //
  59. t1.start();
  60. t2.start();
  61. t1.join();
  62. t2.join();
  63. }
  64. }

如果按照下面的时序执行时序,就会导致死锁:

线程A 线程B
进入a.swap(b)时获得a的锁  
在执行t = getValue()时,顺利获得a的锁(因为已经持有)  进入b.swap(a)时获得b的锁
执行v = other.getValue()时,由于需要b的锁而处于等待的状态 在执行t = getValue()时,顺利获得b的锁
  执行v = other.getValue()时,由于需要a的锁而处于等待状态

以上的代码执行一段时间后可能就会发生死锁。此时可以通过thread dump获得线程的栈跟踪信息。在Unix平台下可以通过向JVM发送SIGQUIT信号(kill -3)获得thread dump,在Windows平台下则通过Ctrl+Break。以上代码在死锁时的thread dump如下:

Found one Java-level deadlock:
    =============================
    "thread2":
      waiting to lock monitor 0x0003e664 (object 0x230c3f40, a Cell),
      which is held by "thread1"
    "thread1":
      waiting to lock monitor 0x0003e6a4 (object 0x230c3f50, a Cell),
      which is held by "thread2"

Java stack information for the threads listed above:
    ===================================================
    "thread2":
            at Cell.getValue(Cell.java:18)
            - waiting to lock <0x230c3f40> (a Cell)
            at Cell.swap(Cell.java:27)
            - locked <0x230c3f50> (a Cell)
            at Cell$2.run(Cell.java:65)
            at java.lang.Thread.run(Unknown Source)
    "thread1":
            at Cell.setValue(Cell.java:22)
            - waiting to lock <0x230c3f50> (a Cell)
            at Cell.swap(Cell.java:29)
            - locked <0x230c3f40> (a Cell)
            at Cell$1.run(Cell.java:46)
            at java.lang.Thread.run(Unknown Source)

Found 1 deadlock.

为了避免死锁的危险,在一个同步的方法或者代码块中,永远不要放弃对客户的控制。换句话说,在一个被同步的区域内部,不要调用一个可被改写的公有或受保护的方法。从包含该同步区域的类的角度来看,这样的一个方法是一个外来者(alien)。这个类不知道该方法会做什么事情,也控制不了它。假设客户的方法创建另一个线程,再回调到这个类中。然后,新建的线程试图获取原线程所拥有的那把锁,这样就会导致新建的线程被阻塞。如果创建该线程的方法正在等待这个线程完成任务,则会导致死锁。   
    另外一种比较简单的避免死锁的独占技术是顺序化资源(resource ordering),它的思想就是把一个嵌套的synchronized方法或块中使用的对象和一个数字标签关联起来。如果同步操作是根据对象标签的最小优先(least first)的原则,那么刚才介绍的例子的情况就不会发生。也就是说,如果线程A和线程B都按照相同的顺序获得锁,就可以避免死锁的发生。对于数字标签的选择,可以使用System.identityHashCode的返回值,尽管没有什么机制可以保证identityHashCode的惟一性,但是在实际运行的系统中,这个方法的惟一性在很大程度上得到了保证。swap的一个更好的实现如下:

    1. public void swap(Cell other)
    2. {
    3. if(this == other) return; // Alias check
    4. else if(System.identityHashCode(this) < System.identityHashCode(other))
    5. {
    6. this.doSwap(other);
    7. }
    8. else
    9. {
    10. other.doSwap(this);
    11. }
    12. }
    13. private synchronized void doSwap(Cell Other)
    14. {
    15. long t = getValue();
    16. long v = other.getValue();
    17. setValue(v);
    18. other.setValue(t);
    19. }

http://whitesock.iteye.com/blog/162344

Object.wait()的使用方法示例(转)的更多相关文章

  1. Object中的clone方法

      Java中对象的创建 clone顾名思义就是复制, 在Java语言中, clone方法被对象调用,所以会复制对象.所谓的复制对象,首先要分配一个和源对象同样大小的空间,在这个空间中创建一个新的对象 ...

  2. ECMAScript5 Object的新属性方法

    虽然说现在并不是所有的浏览器都已经支持ECMAScript5的新特性,但相比于ECMAScript4而言ECMAScript5被广大浏览器厂商广泛接受,目前主流的浏览器中只有低版本的IE不支持,其它都 ...

  3. jquery.validate.min.js 用法方法示例

    页面html 代码 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://w ...

  4. toStirng()与Object.prototype.toString.call()方法浅谈

    一.toString()是一个怎样的方法?它是能将某一个值转化为字符串的方法.然而它是如何将一个值从一种类型转化为字符串类型的呢? 通过下面几个例子,我们便能获得答案: 1.将boolean类型的值转 ...

  5. JavaScript中toStirng()与Object.prototype.toString.call()方法浅谈

    toStirng()与Object.prototype.toString.call()方法浅谈 一.toString()是一个怎样的方法?它是能将某一个值转化为字符串的方法.然而它是如何将一个值从一种 ...

  6. js object 对象 属性和方法的使用

    //object 对象 属性和方法的使用 var person = new Object(); person.name="张海"; person.age="; perso ...

  7. js原型链接(二)和object类的create方法

    原型链的内部执行方式 <script> function Myclass(){ this.x=" x in Myclass"; } var obj=new Myclas ...

  8. Fatal error: Using $this when not in object context in 解决方法

    Fatal error: Using $this when not in object context in 解决方法 粗心造成的错误 $this 只存在于下面情况 $obj = new object ...

  9. Java基础知识强化26:Object类之hashCode()方法、getClass()方法

    1. Object类的hashCode()方法,如下: public  int  hashCode():返回该对象的哈希码值,这个值和地址值有关,但是不是实际地址值(哈希码值是根据实际地址值转化过来的 ...

随机推荐

  1. db2 用户权限

        DB2数据库权限分为实例级权限(SYSADM.SYSCTRL.SYSMAINT.SYSMON)和DB2数据库级权限(DBAMD.LOAD).DB2中用户所拥有的权限主要考虑三个方面:实例级.数 ...

  2. timesetevent与timekillevent的用法

    unit Unit1; interface uses  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs ...

  3. html网页中 点击按钮页面跳转

    在html页面中 实现点击按钮页面跳转.语句如下: <input type="button" value="跳转" onClick="windo ...

  4. php使用 _before_index() 来实现访问页面前,判断登录

    C:\wamp\www\DEVOPS\Home\Lib\Action: <?php class IndexAction extends Action { function index(){ $t ...

  5. EasyUI - LinkButton 按钮控件

    效果: html代码: <div> <a href ="abc.html" id="btn">添加</a> </div ...

  6. webdynpro 组件重用 传值问题

    组件zwd1,需要调用组件zwd2的时候,zwd2组件控制器中需要定义一个方法,定义所要传输的参数,并且该方法需要定义为interface方法. 组件zwd1可以通过代码向导调用组件zwd2,的该方法 ...

  7. ajax基础入门

    补充一下Ajax的使用方法 //可以复制下面两种方法在百度上实验 //jquery的使用方法 $.ajax({ url:"index.php", success:function( ...

  8. Thinkphp入门 二 —空操作、空模块、模块分组、前置操作、后置操作、跨模块调用(46)

    原文:Thinkphp入门 二 -空操作.空模块.模块分组.前置操作.后置操作.跨模块调用(46) [空操作处理] 看下列图: 实际情况:我们的User控制器没有hello()这个方法 一个对象去访问 ...

  9. 利用Xtrabackup备份集合恢复一台从库的过程

    1 time tar -xvf Open..tarx.gz real    35m22.502s user    10m16.499s sys     1m28.578s You have new m ...

  10. NFS服务器端配置

    服务器端配置1 创建共享目录# mkdir /home/share# chown nobody.nogroup /home/share2 创建或修改/etc/exports 配置文件这个文件的内容非常 ...