论坛首页 Java企业应用论坛

并发编程反模式

浏览 3939 次
该帖已经被评为良好帖
作者 正文
   发表时间:2008-10-30  
OO

整理自java并发编程实践第12章 12.4测试方法补遗

 

不连贯的同步性:

为了同步某个对象或者对象本身的某个域的访问,使用同步锁(内部锁或者显式锁,例如,对象本身的内部锁)来保护同步对象.但是如果访问该同步对象没有一贯性地通过同步锁获得访问,就意味着同步策略没有一贯地执行.通俗地讲就是在访问同步对象的时候,采用了双重标准,既有同步的操作也有非同步的操作,破坏了同步的连贯性和完整性.

 

直接调用Thread.run

直接调用Thread的run方法几乎总是个错误,应该调用Thread.start

 

未释放的显式锁

不同于内部锁,显式锁在控制退出了它们被请求的访问的时候,不会自动地释放.标准的技巧是由final块中释放锁;否则如果产生异常,锁仍会保存未释放状态.

		try{
			
		}finally{
			//release lock
		}
 

空synchronized块

空的synchronized块在java存储模型下是有语义的,也是会被执行的.无论开发者试图去解决什么问题,通常会有更好的解决方案.

 

双重检查

在惰性初始化时候,双重检查作为降低同步开销的技巧是有缺陷的.涉及的问题是当读取一个共享可变域的时候,缺乏适当的同步.

双重检查产生的原因是早期jvm缓慢的无竞争同步和缓慢的jvm启动.

双重检查的运作方式是:首先检查在没有同步的情况下,能够被多个线程共享的resource是否被初始化,如果resource不为null就使用它,否则进入初始化的同步块,并再次检查resource是否为null.需要初始化.以便确保只有唯一的线程或者唯一的一次真正地初始化了多个线程共享的resource.通常的代码路径是:获取一个已经构建的resource的引用,并没有用到同步.这就是问题所在,当前线程可能看到一个部分创建的resource.

双重检查的真正问题在于:基于这样的一种假设:当没有使用同步时读取一个共享变量,可能发生的最坏的事情不过是错误地看到过期值(具体而言是null);在这种情况下,通过获取锁后再次检查一次,希望能避免之前看到过期值的风险.但是糟糕的情况是获取到对象的过期值是有效的(具体而言是非空),而当前对象的状态是无效或者错误的(例如是null).这样就会认为对象已经创建成功,而得到一个实际上无效,错误或者为null的resource引用.

public class UnsafeLazyInit {

	private static UnsafeLazyInit resource;
	
	private final static Object initLock = new Object();
	
	private UnsafeLazyInit(){
		
	}
	
	public static UnsafeLazyInit getInstance(){
		if(null == resource){
			synchronized(initLock){
				if(null == resource){
					resource = new UnsafeLazyInit();
				}
			}
		}
		return resource;
	} 
}
 

Java5.0以后,如果把resource声明为volatile类型,双重检查方式就能够很好地工作,因为读取未经缓存过的resource引用.另外jvm做到读取volatile变量相比读取非volatile变量的性能开销并没有增加很多,因此这种方法的性能开销也很低.另外早期jvm缓慢的无竞争同步和缓慢的jvm启动的问题现在已经解决,这种优化的效果也越发不明显.

另外为了确保初始化的安全性,可以通过创建不可变的安全对象在没有同步的状态下,可以被安全地跨线程共享,而不用关心它们是何时发布的.

public class SafeLazyInit {

	private volatile static SafeLazyInit resource;
	
	private final static Object initLock = new Object();
	
	private SafeLazyInit(){
		
	}
	
	public static SafeLazyInit getInstance(){
		if(null == resource){
			synchronized(initLock){
				if(null == resource){
					resource = new SafeLazyInit();
				}
			}
		}
		return resource;
	} 
}
 

一个正确创建的对象,任何可以通过final域访问到的对象或者对象中的域,对看到它的线程都是实时,可见的.另外对于含有final域的对象,final域的初始化安全性可以抑制重排序,否则这些重排序会发生在对象的构造期间以及内部加载对象引用的时刻.

final域的对象只有在对象构造函数完成时才是可见的,对于非final域的对象,或者创建完成后可能改变的值,必须使用同步策略来确保其可见性.

下面的代码是构造安全的,虽然states是使用非线程安全的hashmap,但是在构造器中仍然是安全的,因为states是final域的,并且构造器中的代码是没有重排序,串行执行的.states的引用直到构造器完成之后才对其他线程可见.

import java.util.HashMap;
import java.util.Map;

public class SafeStatus {
	
	private final Map<String,String> states;
	
	public SafeStatus(){
		states = new HashMap<String,String>();
		states.put("color", "blue");
		states.put("length", "123");
		states.put("width", "456");
		states.put("weight", "789");
	}
}
 

由构造函数中启动线程

由构造函数中启动线程,会引入子类化问题的风险,同时还会引起this引用由构造函数中发布溢出.

 

通知错误

notify和notifyAll方法预示着,一个对象的状态可能已经以某种方式发生改变,进而通知那些等待与相关对象关联的条件队列的线程解除堵塞,重新获取条件. 因此只有在与条件队列关联的状态发生改变后才应该去调用这些方法.否则在没有修改任何状态的情形下调用这些方法,通常是一种错误.

 

条件等待错误

在一个队列中等待时,Object.wait和Condition.wait不仅应该在持有正确的锁的情况下,在循环中被调用,而且要在测试过一系列谓词之后.在不持有锁,不在循环中或者没有测试某些状态的情形下调用Object.wait和Condition.wait,通常是一种错误.

 

 

休眠或者等待的时候持有锁

调用Thread.sleep的时持有锁,会导致其他的线程在很长的一段时间内无法执行,因此这是一个潜在的严重的活跃度危险.调用Object.wait和Condition.wait时持有锁也会导致同样的问题.

 

自旋循环

如果代码除了循环检查(忙等待)一个域是否有期望值之外,不做任何事情,就会浪费cpu时间,并且如果域不是volatile类型的,就无法保证循环检查可以终止,因为可能会持有一个过期的状态.如果等待一个状态装换的发生,采用闭锁或者条件等待通常是更好的技术

 

 

老实说java并发编程实践这本书的翻译质量的确不高,不过原著内容写的还是极其精彩.看来接下来要读一些java存储模型方面的内容了.

   发表时间:2008-10-31  
楼主说得是 JSR166 专家组写得那本么? 我本来想买Doug Lea写得那本,没找到找到了Brain写得那本了
0 请登录后投票
   发表时间:2008-10-31  
bloodrate 写道

楼主说得是 JSR166 专家组写得那本么? 我本来想买Doug Lea写得那本,没找到找到了Brain写得那本了

是方妙和韩锴翻译的那本
0 请登录后投票
   发表时间:2008-10-31  
恩,是这两位,这两位比我大一届,是我的学哥和学姐,我知道的他们还翻译了Ant In Aaction,反正我觉得并发的东西本身就不太好解释,不能说是翻译的问题,这本书我看着还是有些帮助了.
0 请登录后投票
   发表时间:2008-10-31  
bloodrate 写道

恩,是这两位,这两位比我大一届,是我的学哥和学姐,我知道的他们还翻译了Ant In Aaction,反正我觉得并发的东西本身就不太好解释,不能说是翻译的问题,这本书我看着还是有些帮助了.

请问您是那一年毕业的??
0 请登录后投票
   发表时间:2008-11-02  
这本书绝对是JAVA并发编程方面的典著
0 请登录后投票
   发表时间:2008-11-06  
当然了,JSR166专家组写的书肯定不会错,其实书中也在到处引用Doug Lea的那本更经典的著作
0 请登录后投票
   发表时间:2008-12-20  
朋友你好, 想听听你的建议, 看看我编写的并行流程实现, 是否有线程安全的方面的问题.

package org.software.sphere.society.platform.pipeline.lang.core.executenode.flow.parallel;

import java.net.ConnectException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.software.sphere.society.platform.pipeline.lang.Session;
import org.software.sphere.society.platform.pipeline.lang.core.executenode.flow.FlowNode;


/**
 * 并行执行类<br>
 * 每个执行流程单独开启一个线程来执行<br>
 * 并且等待它们执行完毕后再执行后面的流程<br>
 * 这里使用了 Thread类中的join方法(注意join方法的调用时间---所有流程开始执行后开始调用)
 * 
 * @author yanchangyou@gmail.com
 * @date : 2008-12-7 下午11:14:05
 * @file : Parallel.java
 * @version : 0.1
 */
public class Parallel extends FlowNode {

	public void execute(final Session clientSession) throws ConnectException, Exception {

		List flowThreadList = new ArrayList();
		for (Iterator iter = flowList.iterator(); iter.hasNext();) {
			final FlowNode flow = (FlowNode) iter.next();
			Thread thread = newThreadExecuteFlow(clientSession, flow);
			flowThreadList.add(thread);
		}
		for (Iterator iter = flowThreadList.iterator(); iter.hasNext();) {
			Thread flowThread = (Thread) iter.next();
			flowThread.join();
		}
	}

	
	public Thread newThreadExecuteFlow(final Session clientSession, final FlowNode flow) {
		Thread thread = new Thread(){ 
			public void run() {
				try {
					flow.execute(clientSession);
				} catch (ConnectException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				} catch (Exception e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		};
		thread.start();
		return thread;
	}
}

0 请登录后投票
   发表时间:2008-12-20   最后修改:2008-12-20
这本书我也买了,翻译得一踏糊涂,很多意思可以说是南辕北辙,有的地方翻译得不知所云。
看完三章之后,又开始啃电子版本了。
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics