`
coolxing
  • 浏览: 870502 次
  • 性别: Icon_minigender_1
  • 来自: 北京
博客专栏
9a45b66b-c585-3a35-8680-2e466b75e3f8
Java Concurre...
浏览量:96098
社区版块
存档分类
最新评论

Race condition--Java Concurrency In Practice C02读书笔记

阅读更多

[本文是我对Java Concurrency In Practice第二章的归纳和总结,  转载请注明作者和出处,  如有谬误, 欢迎在评论中指正. ]

 

多线程环境下,无需调用方进行任何同步处理也能保证正确性的类是线程安全的类

 

无状态的对象是线程安全的。无状态是指没有成员变量。由于方法的局部变量都是在线程私有的栈中分配的,因此在一个线程中调用无状态对象的方法,不会影响到其他线程。

 

race condition: 正确性依赖于事件发生的相对时间。

check-and-act是race condition中的一种,指的是基于check的结果进行操作。由于check和act并非是原子的,进行act时check的结果可能已经无效,那么基于check所进行的act就可能带来问题。

见如下的lazy单例类:

public class LazyInitRace {
	private static ExpensiveObject instance = null;
	public static ExpensiveObject getInstance() {
		// if语句是一个check-and-act操作
		if (instance == null) {
			instance = new ExpensiveObject();
		}
		return instance;
	}
}
 这是一个check-and-act的典型例子:首先判断instance是否为null,如果是就创建ExpensiveObject对象,否则直接返回instance。但是判断和创建并非原子操作,假设线程1判断出instance为null,要是另一个线程紧接着创建了ExpensiveObject对象,那么线程1的判断就失效了,基于判断结果所进行的创建操作就会导致程序中存在多个ExpensiveObject对象--这违背了单例模式的初衷。

 

Read‐modify‐write也是race condition中的一种,指的是读取某个变量的值,修改后写回。这显然不是一个原子操作,如果B线程在A线程read之后write之前修改了变量的值,那么A线程read的结果就失效了,基于read所做的modify就可能带来问题。

见如下的servlet:

public class CountingFactorizer implements Servlet {
	private final long count = 0;
	public void service(ServletRequest req, ServletResponse resp) {
		BigInteger i = extractFromRequest(req);
		BigInteger[] factors = factor(i);
		// // 自增语句是一个Read‐modify‐write操作
		count++;
		encodeIntoResponse(resp, factors);
	}
}
 

使用java提供的同步机制,将check-and-act或者Read‐modify‐write转换为原子操作,则可以避免race condition造成的错误。

使用同步机制改进LazyInitRace和CountingFactorizer类,使之成为线程安全的类:

public class LazyInitRace {
	private static ExpensiveObject instance = null;

	public static ExpensiveObject getInstance() {
		// 使用synchronized将check-and-act操作转换为原子操作
		if (instance == null) {
			synchronized (LazyInitRace.class) {
				if (instance == null) {
					instance = new ExpensiveObject();
				}
			}
		}
		return instance;
	}
}

public class CountingFactorizer implements Servlet {
	private final AtomicLong count = new AtomicLong(0);
	public void service(ServletRequest req, ServletResponse resp) {
		BigInteger i = extractFromRequest(req);
		BigInteger[] factors = factor(i);
		// 使用long对应的原子类AtomicLong,将Read‐modify‐write操作转换为原子操作
		count.incrementAndGet();
		encodeIntoResponse(resp, factors);
	}
}
 

对于只有一个成员变量的对象,成员的状态就是对象的状态。如果涉及到该成员的操作都是原子的,那么该类就是一个线程安全的类。比如以上的LazyInitRace类,其只有一个成员变量instance,且涉及instance的操作是原子的,因此LazyInitRace类是一个线程安全的类。

是否可以类推出,只要类的每一个成员变量的操作都是原子的,类就是线程安全的?不行。

因为每一个成员的操作都是原子的,不能保证所有涉及到成员的操作整体上是原子的。例如:

public class UnsafeCachingFactorizer implements Servlet {
	private final AtomicReference<BigInteger> lastNumber = new AtomicReference<BigInteger>();
	private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<BigInteger[]>();
	public void service(ServletRequest req, ServletResponse resp) {
		BigInteger i = extractFromRequest(req);
		// 优先使用缓存
		if (i.equals(lastNumber.get()))
			encodeIntoResponse(resp, lastFactors.get() );
		else {
			BigInteger[] factors = factor(i);
			// 将最后一次的计算结果缓存起来
			lastNumber.set(i);
			lastFactors.set(factors);
			encodeIntoResponse(resp, factors);
		}
	}
}
 UnsafeCachingFactorizer类的2个成员lastNumber和lastFactors的set()和get()方法是原子的,但是该类不是线程安全的。因为从整体上看,2次写入和2次读取都不是同时进行的,UnsafeCachingFactorizer类仍然存在race condition.

 

java的synchronized机制使用的锁是可重入锁,即同一个线程可以多次申请持有同一把锁而不会引起死锁。假设A线程持有lock,那么如果B线程申请lock锁,B线程就会被阻塞。但是如果A线程再次申请已经持有的lock锁,该申请将获得通过,这就是所谓的同一线程可多次获取同一把锁。作为锁的对象,不仅需要标识锁的所有者,也需要标识所有者持有的计数。如果所有者线程刚刚申请到锁,则计数器的值为1,每重新获取一次,计数器的值加1,每退出一个同步代码块,计数器的值减1. 当计数器的值减为0时,所有者线程才释放锁。可重入锁的设计是为了防止因申请已持有的锁而造成死锁,例如:

public class Widget {
	public synchronized void doSomething() {
		...
	}
}
public class LoggingWidget extends Widget {
	public synchronized void doSomething() {
		System.out.println(toString() + ": calling doSomething");
		super.doSomething();
	}
}
 如果java中的锁不是可重入的,那么调用LoggingWidget对象的doSomething方法就会导致死锁。

 

2
0
分享到:
评论
5 楼 yuanliangding 2016-08-02  
原来这个叫race condition哦。
4 楼 wsong 2015-10-02  
hapjin 写道
lz,请问下,“ 如果java中的锁不是可重入的,那么调用LoggingWidget对象的doSomething方法就会导致死锁。”这句话怎么理解?能详细解释下么?谢谢!

子类的doSomething()覆盖了父类的doSomething(),当子类的doSomething()执行时,synchronized 锁定了子类对象,然后再执行super.doSomething()时,父类的doSomething()的synchronized 需要锁定父类对象吧?

是不是synchronized 不区分子类对象 和 父类的对象?
这样,当锁是可重入的时,在子类LoggingWidget synchronized 锁定了一个对象,再执行super.doSomething()时,又需要锁定一个对象,这两个对象(一个是子类型,一个是父类型)对synchronized 而言,没有区别?

锁定的是同一个对象呀!
子类继承了父类的方法,然后又被重写了,所以从父类继承的方法变成隐式的不会显现,非要调用必须用super.来触发
3 楼 hapjin 2015-09-22  
lz,请问下,“ 如果java中的锁不是可重入的,那么调用LoggingWidget对象的doSomething方法就会导致死锁。”这句话怎么理解?能详细解释下么?谢谢!

子类的doSomething()覆盖了父类的doSomething(),当子类的doSomething()执行时,synchronized 锁定了子类对象,然后再执行super.doSomething()时,父类的doSomething()的synchronized 需要锁定父类对象吧?

是不是synchronized 不区分子类对象 和 父类的对象?
这样,当锁是可重入的时,在子类LoggingWidget synchronized 锁定了一个对象,再执行super.doSomething()时,又需要锁定一个对象,这两个对象(一个是子类型,一个是父类型)对synchronized 而言,没有区别?
2 楼 chuanwang66 2013-08-28  
准备转载到我空间里
1 楼 chuanwang66 2013-08-28  
博主总结得很好啊,我刚开始看这本书,收货颇大:)

相关推荐

Global site tag (gtag.js) - Google Analytics