以前我认为多线程编程只需注意:
·能并发的,要尽量用并发提升性能,比如分词,如果要对一片文章分词,很想当的采用单线程,若采用多线程可以大大的提升效率(按照段落分配任务给多线程)。
·要注意临界区(race condition),对于这些区要用java中intrinsic lock,确保操作的原子性。以防止共享变量之间的不一致性。
·当线程比较多时,采用线程池来调优。
我所理解的多线程仅仅如此。
今天读了《java concurrent in practice》的 sharing objects部分,感觉自己以前对多线程技术的掌握还远远没有入门。
共享对象概要:
intrinsic lock并非仅仅只是确保操作的序列性和原子性,其重中之重是线程对mutable 变量(对象)的可见性。如果没有对mutable变量(或者对象)的可见性,那么程序的行为会产生难以排查的BUG。 如下面代码:
public class NoVisibility {
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread {
public void run() {
while (!ready)
Thread.yield();
System.out.println(number);
}
}
public static void main(String[] args) {
new ReaderThread().start();
number = 42;
ready = true;
}
}
ReaderThread可能永久的无法跳出循环,原因就是 ready和number对于其他线程(不是主线程)来说是不可见的。由此可见,对象的可见性有多重要。
那么问题来了,如何保证对象对于引用他的线程是可见的?
1)添加volatile关键字,volatile修饰的变量是对于多线程来说是可见的,但是volatile关键字不保证造作的原子性。这也就是编程时,为何不要对volatile修饰的整型进行++操作;
2)用intrinsic lock(也就是synchronized关键字,这个单词比较难拼写,所以用intrinsic lock替代),用intrinsic lock修饰的方法或者代码片段,对于多线程来说是可见的,且保证了操作的原子性。
3)把对象或者变量放入线程安全的容器中,比如HashTable,Vector等java线程安全容器。
4)对于不可变共享变量,用于多线程环境中,要用final关键字形容,仅仅是public static 形容的变量是不可见的,会产生不可预知的风险。
以上都是基于一条准则:对象的引用对其他线程可见,并不意味着对象的状态一定对消费线程可见。
后话:
当获得一个对象的引用时,要知道可以用这个对象来做什么。是否需要在使用它之前先获得一个锁?是否可以修改它的状态,还是仅仅可读?在进行多线程编程时许多错误都是因为我们没有对即将操作的对象的这些“预设规则”的深入理解导致的。所以写多线程编程时,对于涉及到的共享对象,最好要有一个文档说明。