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

Java Concurrent (2)

阅读更多

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

 

1.3 原子数据的同步

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

private static int nextSerialNumber = 0;
public static int generateSerialNumber()
{
    return nextSerialNumber++;
}

    这个程序的意图是保证每次调用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()方法的标准模式:

synchronized(obj)
{
    while( condition checking)
    {
       obj.wait();
    }
    …// Other operations
}

    总是要使用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的锁。因此没有哪个线程能够执行进一步的操作,死锁就产生了。例如:

public class Cell {
	private long value;
	
	public Cell(long value) {
		this.value = value;
	}
	
	public synchronized long getValue() {
		return value;
	}

	public synchronized void setValue(long value) {
		this.value = value;
	}
	
	public synchronized void swap(Cell other) {
		long t = getValue();  
		long v = other.getValue();  
		setValue(v);  
		other.setValue(t);
	}
	
	public static void main(String args[]) throws Exception {
		//
		final Cell c1 = new Cell(100);
		final Cell c2 = new Cell(200);
		
		//
		Thread t1 = new Thread(new Runnable() {
			public void run() {
				long count = 0;
				try {
					while(true) {
						c1.swap(c2);
						count++;
						if(count % 100 == 0) {
							System.out.println("thread1's current progress: " + count);
						}
					}
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		});
		t1.setName("thread1");
		
		//
		Thread t2 = new Thread(new Runnable() {
			public void run() {
				long count = 0;
				try {
					while(true) {
						c2.swap(c1);
						count++;
						if(count % 100 == 0) {
							System.out.println("thread2's current progress: " + count);
						}
					}
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		});
		t2.setName("thread2");
		
		//
		t1.start();
		t2.start();
		t1.join();
		t2.join();
	}
}
  

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

线程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的一个更好的实现如下:

public void swap(Cell other)
{
    if(this == other) return; // Alias check
    else if(System.identityHashCode(this) < System.identityHashCode(other))
    {
        this.doSwap(other);
    }
    else
    {
        other.doSwap(this);
    }
}

private synchronized void doSwap(Cell Other)
{
    long t = getValue();
    long v = other.getValue();
    setValue(v);
    other.setValue(t);
}
3
0
分享到:
评论
1 楼 huibin147852369 2014-04-11  

System.identityHashCode(this) < System.identityHashCode(other)

为什么this 的hashcode 要小于other 的hashcode 呢?

相关推荐

    java concurrent 精简源码

    本资源“java concurrent 精简源码”着重关注Java并发库(java.util.concurrent)的核心概念,包括阻塞队列和线程管理。下面将详细阐述这些知识点。 1. **Java并发库(java.util.concurrent)** Java并发库是Java ...

    Java Concurrent in practice (animated)

    Java Concurrent in practice (animated)

    Java Concurrent Programming

    为了简化多线程编程,Java提供了一系列工具和API,如`java.util.Timer`和`java.util.concurrent`包,这些工具可以帮助开发者更高效地管理线程间的同步问题。 ##### 1.2 synchronized关键字 `synchronized`关键字是...

    java concurrent 包 详细解析

    2. **Executor框架**:`java.util.concurrent.Executor`是执行任务的核心接口,它定义了运行任务的方法。`ExecutorService`是Executor的一个子接口,提供了管理和控制执行器的额外功能,如`shutdown()`用于关闭执行...

    JAVA的CONCURRENT用法详解.pdf

    JAVA的CONCURRENT用法详解.pdf

    使用java concurrent调用xmlp api生成pdf

    这里我们关注的是如何使用`java.concurrent`包中的工具和XML Processing API(通常指的是JAXB或DOM4J等处理XML的库)来高效地生成PDF。下面将详细解释这个过程以及涉及的相关知识点。 首先,`java.concurrent`包是...

    java concurrent source code

    资深Java专家10年经验总结,全程案例式讲解,首本全面介绍Java多线程编程技术的专著 结合大量实例,全面讲解Java多线程编程中的并发访问、线程间通信、锁等最难突破的核心技术与应用实践 封底 Java多线程无处不在,...

    java并发工具包 java.util.concurrent中文版用户指南pdf

    1. java.util.concurrent - Java 并发工具包 2. 阻塞队列 BlockingQueue 3. 数组阻塞队列 ArrayBlockingQueue 4. 延迟队列 DelayQueue 5. 链阻塞队列 LinkedBlockingQueue 6. 具有优先级的阻塞队列 ...

    java并发工具包 java.util.concurrent中文版pdf

    ### Java并发工具包 `java.util.concurrent` 知识点详解 #### 一、引言 随着多核处理器的普及和应用程序复杂度的增加,多线程编程成为了现代软件开发不可或缺的一部分。为了简化并发编程的复杂性,Java 5 引入了 `...

    java concurrent包分类结构图

    java concurrent包分类结构图

    JAVA Concurrent Programming

    2. Java 1.5+并发工具 Java 1.5引入了`java.util.concurrent`包,包含了一系列的并发工具类,如线程池、阻塞队列、并发集合等。这些工具旨在提高并发性能并简化编程模型。例如,`ExecutorService`和`...

    Java Concurrent处理并发需求

    ### Java Concurrent处理并发需求 #### 一、Java并发基础与Concurrent API介绍 在现代软件开发中,尤其是在服务器端应用中,对并发处理的需求日益增长。为了满足这种需求,Java平台提供了一系列强大的工具和API来...

    java concurrent program的实现

    EBS java concurrent program的实现

    java concurrent in practive

    《Java并发编程实战》还会讨论`java.util.concurrent`包中的高级并发工具,如`ExecutorService`和`Future`,它们可以方便地管理和控制线程池,提高系统的并行处理能力。`CountDownLatch`、`CyclicBarrier`和`...

    java同步大杀器concurrent 包

    java同步大杀器concurrent 包java同步大杀器concurrent 包java同步大杀器concurrent 包java同步大杀器concurrent 包java同步大杀器concurrent 包java同步大杀器concurrent 包java同步大杀器concurrent 包java同步大...

    java_util_concurrent_中文用户使用手册@微信公众号-架构探险之道.zip

    JUC使用指导手册 http://tutorials.jenkov.com/java-util-concurrent/blockingqueue.html 中文译文

    Concurrent Programming in Java

    2. **同步机制**:Java提供了多种同步工具,如`synchronized`关键字、`wait()`、`notify()`和`notifyAll()`方法,用于控制对共享资源的访问。书中详细解释了这些机制的工作原理和使用场景,以及死锁、活锁和饥饿等...

    动画学习 java.util.concurrent并发工具包

    如何启动:以win7系统为例,最好jdk8 1.打开cmd,cd到jdk的path,本机是:cd C:\Java\jdk6\bin ...java -cp D:\javaConcurrentAnimated.jar vgrazi.concurrent.samples.launcher.ConcurrentExampleLauncher

    使用Java并发编程Concurrent Programming Using Java

    Java平台提供了丰富的API支持并发编程,如`java.util.concurrent`包下的各种类和接口,这些工具可以帮助开发者更高效地管理多线程环境下的任务调度和数据共享问题。 ### Java并发编程基础 #### 1. 多线程基础 - **...

    java.concurrent包的应用

    Java并发包 (`java.concurrent`) 是Java平台中处理多线程并行执行的关键工具,它提供了高效、可控且安全的线程管理机制。在后台程序设计中,尤其对于处理大量并发请求的服务,如Web服务器、短信服务器或DNS服务器,...

Global site tag (gtag.js) - Google Analytics