java多线程并发(二)(线程基础)

转自:http://blog.psjay.com/posts/summary-of-java-concurrency-two-synchronized-and-atomicity/

再来考虑一下前几天发生的事情。因为日本地震海啸以及核爆炸的缘故,有人造谣说,咱国内已经受到了核污染,吃含碘的东西能够减轻核辐射带来的影响。于是就有投机的人在淘宝上开了一家网店,专卖碘片,一块钱一片。生意十分火爆。有很多个买家不断地在买碘片,一直到把钱给用光。买家买碘片的这些钱都打到了卖家的同一个银行账号里。所以,结果就是,买家所有的钱最后都到了卖家的银行账户里,卖家银行账号里的总额就是所有买家在买碘片之前的现金总计。

所以可以这样来设计类:

/**
 * 银行账户类
 * 
 */
public class BankAccount {

	private int total = 0;

	public void add(int n) {
		total += n;
	}

	public int getTotal() {
		return total;
	}
}

/**
 * 买家类
 * 
 */

public class Customer implements Runnable {

	private BankAccount account;
	private int cash;

	public Customer(int cash, BankAccount account) {
		this.cash = cash;
		this.account = account;
	}

	public void cost(int n) {
		cash -= n;
		account.add(n);
	}

	@Override
	public void run() {
		while (cash > 0) { // 直至将钱用光
			cost(1);
		}
		System.out.println("total: " + account.getTotal()); // 打印出银行账户的总计金额
	}
}

public class Test {

	public static void main(String[] args) {

		BankAccount account = new BankAccount();
		for (int i = 0; i < 100; i++) {
			new Thread(new Customer(100000, account)).start();
		}
	}

}

正如代码所示,有100个聪明的向往健康长寿的又有钱的买家各自用10万块不断地狂买碘片。

你可能注意到了,BankAccount类的add()方法并不是synchronized的。为什么呢?因为add()方法里只有一句话,那就是“total += n;”,所以不会出现第一个例子中那样多句话执行到一半被打断的问题。这是正确的么?实践是检验真理的唯一标准,上述程序的某次运行结果如下:

(省略了N行)
total: 7861909
total: 7881906
total: 7995946
total: 8001495
total: 8081441

oops!居然出问题了!最后卖家银行账户里的总额并不是100*100000 = 10000000。看吧,无故少了这么多钱,有谁会愿意去这样的“吞钱”银行开设账户呢?那么问题究竟出在哪里了呢?BankAccount的add()方法不是只有一句话么,难道这一句话也能被打断?回答就是:这一句话确实能够被打断,因为这样的操作不具有原子性(atomicity),关于原子性稍后再总结。先谈谈怎么解决这个银行吞钱的问题。当然,如上面所说的,给add()方法和getTotal()加上synchronized就行了。

synchronized除了能修饰方法之外,还能创建同步块。如果有时候一个方法里面只有几句话需要同步,你可以考虑这种写法:

public void doSomething() {
		// 一些操作
		synchronized (this) {
			// 一些需要被同步的操作
		}
		// 另外一些操作
	}

其中,synchronized后的括号内必须要为一个对象。表示:要执行同步块里的这些操作一定要当前线程取得括号内的这个对象的锁才行。常用的就是this,表示当前对象。在一些高级应用中,可能会用到其他对象的锁。

你也可以显式的使用锁对象来实现同步,Java提供了一些Lock类,本篇总结中不打算包含这些内容。

原子性(atomicity)

具有原子性的操作被称为原子操作。原子操作在操作完毕之前不会线程调度器中断。在Java中,对除了long和double之外的基本类型的简单操作都具有原子性。简单操作就是赋值或者return。比如”a = 1;“和 “return a;”这样的操作都具有原子性。但是在Java中,上面买碘片例子中的类似”a += b”这样的操作不具有原子性,所以如果add方法不是同步的就会出现难以预料的结果。在某些JVM中”a += b”可能要经过这样三个步骤:

  1. 取出a和b

  2. 计算a+b

  3. 将计算结果写入内存

如果有两个线程t1,t2在进行这样的操作。t1在第二步做完之后还没来得及把数据写回内存就被线程调度器中断了,于是t2开始执行,t2执行完毕后t1又把没有完成的第三步做完。这个时候就出现了错误,相当于t2的计算结果被无视掉了。所以上面的买碘片例子在同步add方法之前,实际结果总是小于预期结果的,因为很多操作都被无视掉了。

类似的,像”a++“这样的操作也都不具有原子性。所以在多线程的环境下一定要记得进行同步操作。有一些并发大牛可以利用原子性避免同步而写出“免锁”的代码。Goetz开玩笑说:

如果你能编写出一个牛逼的高性能的JVM,你就可以考虑考虑是否可以避免使用同步。

所以,在成为这样牛的大牛之前,还是老老实实使用同步吧。

Java SE引入了原子类,比如AtomicInter,AtomicLong等等。

volatile

上面提到了,对long和double的简单操作不具有原子性。但是,一旦给这两个类型的属性加上volatile修饰符,对它们的简单操作就会具有原子性(当然这是说的在Java SE5之后的故事)。

在一些情况下即便是原子操作也可能会引发一些错误,特别是在多处理器的环境下。因为多处理器的计算机可以将内存中的值暂时储存在寄存器或者本地内存缓冲区中。所以,运行在不同处理器上的线程取同一个内存位置的值可能不相同。有一些编译器也会自作主张地优化指令,使得上述情况发生。你当然可以用同步锁来解决这些问题,不过volatile也能解决。

如果给一个变量加上volatile修饰符,就相当于:每一个线程中一旦这个值发生了变化就马上刷新回主存,使得各个线程取出的值相同。编译器不要对这个变量的读、写操作做优化。

但是值得注意的是,除了对long和double的简单操作之外,volatile并不能提供原子性。所以,就算你将一个变量修饰为volatile,但是对这个变量的操作并不是原子的,在并发环境下,还是不能避免错误的发生!比如在碘片例子中,将BankAccount类写成这样:

即便total被volatile修饰,但是由于add方法不是同步的,所以不能避免错误的发生!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章