- 浏览: 56860 次
- 性别:
- 来自: 成都
文章分类
最新评论
-
chiqinghaichi:
楼主好!菜鸟一枚,试了下你的程序,当访问 http://loc ...
SpringMVC -
随意而生:
复习复习 不错
SpringMVC -
ccii:
cylboke 写道他的意思是“orcle”写错了吧?我猜的! ...
Oracle基本操作 -
happy200318:
给个赞,不错
SpringMVC -
cylboke:
他的意思是“orcle”写错了吧?我猜的!!!
Oracle基本操作
本章内容:
1. 同步访问共享的可变数据
2. 避免过度同步
3. executor和task优先干线程
4. 并发工具优先于wait和notify
5. 线程安全性的文档化
6. 慎用延迟初始化
7. 不要依赖于线程调度器
8. 避免使用线程组
1. 同步访问共享的可变数据
关键字synchronized可以保证在同一时刻,只有一个线程可以执行某一个方法,或者某一个代码块。对象同步并不仅限于当多个线程操作同一可变对象时,仍然能够保证该共享对象的状态始终保持一致。与此同时, 他还可以保证进入同步方法或者同步代码块的每个线程,都看到由同一个锁保护的之前所有的修改效果。
Java语言规范保证了读写一个变量是原子的,除非这个变量的类型为long 或double。换句话说, 读取一个非long 或double 类型的变量,可以保证返回的值是某个线程保存在该变量中的,即时多个线程在没有同步的情况下并发地修改这个变量也是如此。你可能听说过,为了提高性能,在读或写原子数据的时候,应该避免使用同步,这个建议是非常危险而错误的。虽然语言规范保证了线程在读取原子数据的时候,不会看到任意的数值,但是它并不保证一个线程写入的值对于另一个线程将是可见的。为了在线程之间进行可靠的通信,也为了互斥访问,同步是必要的。
即便这样做不会带来数据同步修改的问题,但是他会导致另外一个更为隐匿的错误发生。见如下代码:
public class StopThread {
private static boolean stopRequested = false;
public static void main(String[] args) throw InterruptedException
{
Thread bgThread = new Thread(new Runnable() {
public void run() {
int i = 0;
while (!stopRequested)
i++;
}
});
bgThread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}
对于上面的代码片段,有些人会认为在主函数sleep 一秒后,工作者线程的循环状态标志(stopRequested)就会被修改,从而致使工作者线程正常退出。然而事实却并非如此,因为Java 的规范中并没有保证在非同步状态下,一个线程修改的变量,在另一个线程中就会立即可见。事实上,这也是Java针对内存模型进行优化的一个技巧。没有同步,虚拟机将这个代码:
while (!stopRequested)
i++;
转换成这样:
if (!stopRequested) {
while (true)
i++;
}
这是可以接收的,这种优化被称为提升,正是HotSpot Server VM的工作。结果是个活性失败:这个程序无法前进。修正这个问题的一种方式是同步访问stopRequested域。见如下代码:
public class StopThread {
private static boolean stopRequested = false;
private static synchronized void requestStop() {
stopRequested = true;
}
private static synchronized boolean stopRequested() {
return stopRequested;
}
public static void main(String[] args) throw InterruptedException
{
Thread bgThread = new Thread(new Runnable() {
public void run() {
int i = 0;
while (!stopRequested())
i++;
}
});
bgThread.start();
TimeUnit.SECONDS.sleep(1);
requestStop();
}
}
在上面的修改代码中,读写该变量的函数均被加以同步。如果读和写方法没有都被同步,同步就不会起作用。
StopThread中被同步方法的动作即使没有同步也是原子的。换句话说,这些方法的同步只是为了它的通信效果,而不是为了互访访问。事实上,Java 中还提供了另外一种方式用于处理该类问题,即volatile 关键字。该单词的直译为“易变的”,引申到这里就是告诉cpu 该变量是容易被改变的变量,不能每次都从当前线程的内存模型中获取该变量的值,而是必须从主存中获取,这种做法所带来的唯一负面影响就是效率的折损,但是相比于
synchronized 关键字,其效率优势还是非常明显的。见如下代码:
public class StopThread {
private static volatile boolean stopRequested = false;
public static void main(String[] args) throw InterruptedException
{
Thread bgThread = new Thread(new Runnable() {
public void run() {
int i = 0;
while (!stopRequested)
i++;
}
});
bgThread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}
和第一个代码片段相比,这里只是在stopRequested 域变量声明之前加上volatile 关键字,从而保证该变量为易变变量。然而需要说明的是,该关键字并不能完全取代synchronized 同步方式,见如下代码:
public class Test {
private static volatile int nextID = 0;
public static int generateNextID() {
return nextID++;
}
}
generateNextID方法的用意为每次都给调用者生成不同的ID 值,遗憾的是,最终结果并不是我们期望的那样,当多个线程调用该方法时,极有可能出现重复的ID 值。问题在于增量操作符(++)不是原子的,而是由两个指令构成,首先是读取一个值,然后写回一个新值。由此可见,这两个指令之间的时间窗口极有可能造成数据的不一致。
修正generateNextID方法的一种方法是在它的声明中增加synchronized修饰符,并删除volatile修饰符。另一种方法是使用JDK(1.5 later)中java.util.concurrent.atomic包提供的AtomicLong类,使用该类性能要明显好于synchronized 的同步方式,见如下修复后的代码:
public class Test {
private static final AtomicLong nextID = new AtomicLong();
public static long generateNextID() {
return nextID.getAndIncrement();
}
}
避免本条目中所讨论的问题的最佳办法是不共享可变的数据,要么共享不可变的数据,要么压根不共享。
简而言之,当多个线程共享可变数据的时候,每个读或者写数据的线程都必须执行同步。如果没有同步,就无法保证一个线程所做的修改可以被另一个线程获知。如果只需要线程之间的交互通信,而需要互斥,volatile修饰符就是一种可以接受的同步形式。
2. 避免过度同步
过度同步所导致的最明显问题就是性能下降,特别是在如今的多核时代,再有就是可能引发的死锁和 一系列不确定性的问题。
当同步函数或同步代码块内调用了外来方法,如可被子类覆盖的方法,或外部类的接口方法等。由于这些方法的行为存在一定的未知性,如果在同步块内调用了类似的方法,将极有可能给当前的同步带来未知的破坏性。见如下代码:
public class ObservableSet<E> extends ForwardingSet<E> {
public ObservableSet(Set<E> set) {
super(set);
}
private final List<SetObserver<E>> observers = new ArrayList<SetObserver<E>>();
public void addObserver(SetObserver<E> observer) {
synchronized(observers) {
observers.add(observer);
}
}
public boolean removeObserver(SetObserver<E> observer) {
synchronized(observers) {
return observers.remover(observer);
}
}
private void notifyElementAdded(E element) {
synchronized(observers) {
for (SetObserver<E> observer : observers)
observer.added(this,element);
}
}
@Override public boolean add(E element) {
boolean added = super.add(element);
if (added)
notifyElementAdded(element);
return added;
}
@Override public boolean addAll(Collection<? extends E> c) {
boolean result = false;
for (E element : c)
result |= add(element);
return result;
}
}
Observer通过调用addObserver方法预定通知,通过调用removeObserver方法取消预定。在这两种情况下,这个回调接口的实例都会被传递给方法:
public interface SetObserver<E> {
void added(ObservableSet<E> set,E element);
}
如果只是粗略地检测一下,ObservableSet会显得很正常。如下:
public static void main(String[] args) {
ObservableSet<Integer> set = new ObservableSet<Integer>(new HashSet<Integer>());
set.addObserver(new SetObserver<Integer>() {
public void added(ObservableSet<Integer> s, Integer e) {
System.out.println(e);
}
});
for (int i = 0; i < 100; i++)
set.add(i);
}
对于这个测试用例,他完全没有问题,可以保证得到正确的输出,即打印出0-99 的数字。现在我们换一个观察者接口的实现方式,见如下代码片段:
set.addObserver(new SetObserver<Integer>() {
public void added(ObservableSet<Integer> s,Integer e) {
System.out.println(e);
if (e == 23)
s.removeObserver(this);
}
});
对于以上代码,你可能以为这个程序会打印出0~23的数字,之后观察者会取消预定,程序会悄悄地完成它的工作。实际上却是打印出0~23的数字,然后抛出ConcurrentModificationException 异常。问题在于,当notifyElementAdded调用观察者的added方法时,它正处于遍历observers列表的过程中,added方法调用可观察集合的removeObserver方法,从而调用observers.remove。现在有麻烦了,我们正在企图在遍历列表的过程中,将一个元素从列表中删除,这是非法的。notyfyElementAdded方法中的迭代是在一个同步的块中,可以防止并发的修改,但是无法防止迭代线程本身回调到可观察的集合中,也无法防止修改它的observers列表。
下面看另一个例子,编写一个试图取消预定的观察者,但是不直接调用removeObserver,它用另一个线程的服务来完成。这个观察者使用了一个executor service:
set.addObserver(new SetObserver<Integer>() {
public void added(final ObservableSet<Integer> s,Integer e) {
System.out.println(e);
if (e == 23)
ExecutorService executor = Executors.newSingleThreadExecutor();
final SetObserver<Integer> observer = this;
try{
executor.submit(new Runnable(){
public void run(){
s.removeObserver(observer);
}
}).get();
}catch(ExecutionException ex){
throw new AssertionError(ex.getCause());
}catch(InterruptedException ex){
throw new AssertionError(ex.getCause());
}finally{
executor.shutdown();
}
}
}
});
这一次我们没有遇到异常,而是遭遇了死锁。后台线程调用s.removeObserver,它企图锁定observers,但它无法获得该锁,因为主线程已经有锁了。在这期间,主线程一直等待后台线程来完成对观察者的删除,这正是造成死锁的原因。
由于Java中synchronized 关键字构成的锁是可重入的,或者说是可递归的,即在同一个线程内可多次调用且不会被阻塞,这种调用不会死锁,就像第一个例子,它会产生一个异常。如果恰恰相反,我们的冲突调用来自于多个线程,那么将会形成死锁。在多线程的应用程序中,死锁是一种比较难以重现和定位的错误。
为了解决上述问题,我们需要做的一是将调用外部代码的部分移出同步代码块,再有就是针对该遍历,我们需要提前copy 出来一份,并基于该对象进行遍历,从而避免了上面的并发访问冲突,如下:
private void notifyElementAdded(E element) {
List<SetObserver<E>> snapshot = null;
synchronized(observers) {
snapshot = new ArrayList<SetObserver<E>>(observers);
}
for (SetObserver<E> Observer : snapshot)
Observer.added(this,element);
}
事实上,还有一种更好的方法,Java1.5以来,Java类库就提供了一个并发集合,叫做CopyOnWriteArrayList,这是专门为此定制的。它通过重新拷贝整个底层数组,在这里实现所有的写操作。由于内部数组永远不改动,因此迭代不需要锁定,速度也非常快。修改如下:
public class ObservableSet<E> extends ForwardingSet<E> {
public ObservableSet(Set<E> set) {
super(set);
}
private final List<SetObserver<E>> observers = new CopyOnWriteArrayList<SetObserver<E>>();
... ...
上面的两个修改都避免了出现异常和死锁。
通常,你应该在同步区域内做尽可能少的工作。获得锁,检查共享数据,根据需要转换数据,然后放掉锁。如果你必须要执行某个耗时的动作,则应设法把这个动作移到同步区域的外面。
在这个多核的时代,过度同步的实际成本并不是指获取锁所花费的CPU时间,而是指失去了并行的机会,以及因为需要确保每个核都有一个一致的内存视图而导致的延迟。过度同步的另一项潜在开销在于,它会限制VM优化代码执行的能力。
如果一个可变的类要并发使用,应该使这个类变成是线程安全的,通过内部同步,你还可以获得明显比从外部锁定整个对象更高的并发性。否则,就不要在内部同步,让客户在必要的时候从外部同步。减少不必要的代码同步还可以大大提高程序的并发执行效率,一个非常明显的例子就是StringBuffer,该类在JDK 的早期版本中即以出现,是数据操作同步类,即时我们是以单线程方式调用该类的方法,也不得不承受块同步带来的额外开销。Java 在1.5 中提供了非同步版本的StringBuilder 类,这样在单线程应用中可以消除因同步而带来的额外开销,对于多线程程序,可以继续选择StringBuffer,或者在自己认为需要同步的代码部分加同步块。 所以,当你不确定的时候,就不要同步你的类,而是应该建立文档,注明它不是线程安全的。
简而言之,为了避免死锁和数据破坏,千万不要从同步区域内部调用外来方法。更为一般的讲,要尽量的限制同步区域内部的工作量。当你在设计一个可变类的时候,要考虑一下它们是否应该自己完成同步操作。在现在这个多核时代,这比永远不要过度同步来得更重要。只有当你有足够的理由一定要在内部同步类的时候,才应该这么做,同时还应该将这个决定清楚地写到文档中。
3. executor和task优先于线程
在Java 1.5 中提供了java.util.concurrent,在这个包中包含了Executor Framework 框架, 这是一个很灵活的基于接口的任务执行工具。该框架提供了非常方便的调用方式和强大的功能,如:
ExecutorService executor = Executors.newSingleThreadExecutor(); //创建一个单线程执行器对象。
executor.execute(runnable); //提交一个待执行的任务。
executor.shutdown(); //使执行器优雅的终止。
你可以利用executor service完成更多的事情,如:可以等待完成一项目特殊的任务,可以等待executor service优雅地完成终止,可以在任务完成时逐个获取这些任务的结果,等等。
如果想让不止一个线程来处理来自这个队列的请求,只要调用一个不同的静态工厂,这个工厂创建了一种不同的executor service,称作线程池。你可以用固定或者可变数目的线程创建一个线程池。java.util.concurrent.Executors类包含了静态工厂,能为你提供所需的大多数executor。然而,如果你想要来点特别的,可以直接使用ThreadPoolExecutor类。这个类允许你控制线程池操作的几乎每个方面。
为特殊的应用程序选择executor service是很有技巧的。如果编写的是小程序,或者是轻载的服务器,使用Executors.newCachedThreadPool()通常是个不错的选择,因为它不需要配置,并且在一般情况下能够正确地完成工作。但是对于大负载的服务器来说,缓存的线程池就不是很好的选择了,在缓存的线程池中,被提交的任务没有排成队列,而是直接交给线程执行。如果没有线程可用,就创建一个新的线程。如果服务器负载得太重,以致它所有的CPU都全被占用了,当有更多的任务时,就会创建更多的线程,这样只会使情况变得更糟。对于该种情况,Executors 提供了另外一个工厂方法Executors.newFixedThreadPool(),它为你提供了一个包含固定线程数目的线程池,或者为了最大限度地控制它,就直接使用ThreadPoolExecutor类。
Executor Framework也有一个可以代替java.util.Timer的东西,即ScheduledThreadPoolExecutor,通过工厂方法Executors.ScheduledThreadPool()可以创建该类。虽然timer使用起来更加容易,但是被调用的线程池executor更加灵活。timer只用一个线程来执行任务,这在面对长期运行的任务时,会影响到定时的准确性。如果timer唯一的线程抛出未被捕获的异常,timer就会停止执行。被调度的线程池executor支持多个线程,并且优雅地从抛出未受检异常的任务中恢复。
4. 并发工具优先于wait和notify
自从Java1.5发行版本开始,Java平台就提供了更高级的并发工具,它们可以完成以前必须在wait和notify上手写代码来完成的各项工作。既然正确地使用wait和notify比较困难,就应该用更高级的并发工具来代替。
java.util.concurrent 中更高级的工具分成三类:Executor Framework(在3中简单说明)、并发集合(Concurrent Collection)以及同步器(Synchronizer)。
并发集合为标准的集合接口提供了高性能的并发实现。为了提高并发性,这些实现在内部管理同步,并发集合不可能排除并发活动,将它锁定没有什么作用,只会使程序的速度变慢。
有些集合接口已经通过依赖状态的修改操作进行了扩展,将几个基本操作合并到了单个原子操作中。如java.util.concurrent 中提供的并发集合就有更好的并发性,其性能通常数倍于普通集合。如ConcurrentHashMap,它扩展了Map接口,并添加了几个方法,包括putIfAbsent(key, value),当键没有映射时会替它插入一个映射,并返回与键关联的前一个值,如果没有这样的值,则返回null,ConcurrentHashMap除了提供卓越的并发性之外,速度也非常快。换句话说,除非有极其特殊的原因存在,否则在并发的情况下,一定要优先选择ConcurrentHashMap,而不是Collections.syschronizedmap 或者Hashtable。
java.util.concurrent 包中还提供了阻塞队列,它们会一直等待到可以成功执行为止。如BlockingQueue扩展了Queue接口,并添加了包括take在内的几个方法,它从队列中删除并返回了头元素,如果队列为空就等待。这样就允许将阻塞队列用于工作队列,也称作生产者-消费者队列,大多数ExecutorService实现都使用BlockingQueue,该队列极大的简化了生产者线程和消费者线程模型的编码工作。
同步器是一些使线程能够等待另一个线程的对象,允许它们协调动作。包括常用的CountDownLatch(倒计数锁存器)和Semaphore,和不常用的CyclicBarrier 和Exchanger。
CountDownLatch是一次性的障碍,允许一个或者多个线程等待一个或者多个线程来做某些事情。CountDownLatch 的唯一构造函数带有一个int 类型的参数,这个int 参数是指允许所有在等待的线程被处理之前,必须在锁存器上调用countDown 方法的次数。
现在我们给出一个简单应用场景,然后再给出用CountDownLatch 实现该场景的实际代码。场景描述如下:假设想要构建一个简单的框架,用来给一个动作的并发执行定时。这个框架中包含单个方法,这个方法带有一个执行该动作的executor,一个并发级别(表示要并发执行该动作的次数),以及表示该动作的runnable。所有的工作线程自身都准备好,要在timer 线程启动时钟之前运行该动作。当最后一个工作线程准备好运行该动作时,timer 线程就开始执行,同时允许工作线程执行该动作。一旦最后一个工作线程执行完该动作,timer 线程就立即停止计时。直接在wait 和notify 之上实现这个逻辑至少来说会很混乱,而在CountDownLatch 之上实现则相当简单。见如下示例代码:
public static long time(Executor executor,int concurrency,final Runnable action) {
final CountDownLatch ready = new CountDownLatch(concurrency);
final CountDownLatch start = new CountDownLatch(1);
final CountDownLatch done = new CountDownLatch(concurrency);
for (int i = 0; i < concurrency; i++) {
executor.execute(new Runnable() {
public void run() {
ready.countDown();
try {
start.await();
action.run();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
done.countDown();
}
}
});
}
//等待工作者线程准备可以执行,即所有的工作线程均调用ready.countDown()方法。
ready.await();
//这里使用nanoTime,是因为其精确度高于System.currentTimeMills(),且不受系统的实时时钟的调整所影响。
long startNanos = System.nanoTime();
//该语句执行后,工作者线程中的start.await()均将被唤醒。
start.countDown();
//下面的等待,只有在所有的工作者线程均调用done.countDown()之后才会被唤醒。
done.await();
return System.nanoTime() - startNanos;
}
注意这个方法使用了三个倒计数锁存器,第一个是ready,工作线程用它来告诉timer线程它们已经准备好了。然后工作线程在第二个锁存上等待,也就是start。当最后一个工作线程调用ready.countDown时,timer线程记录下起始时间,并调用start.countDown,允许所有的工作线程继续进行。然后timer线程在第三个锁存器上等待,直到最后一个工作线程运行完该动作,并调用done.countDown。一旦调用这个,timer线程就会苏醒过来,并记录下结束的时间。
虽然你始终应该优先使用并发工具,而不是使用wait和notify,但可能必须维护使用了wait和notify的遗留代码。wait方法被用来使线程等待某个条件,它必须在同步区域内部被调用,这个同步区域将对象锁定在了调用wait方法的对象上。下面是使用wait方法的标准模式:
synchronized(obj){
while(<condition does not hold>)
obj.wait()
... ...
}
始终应该使用wait循环模式调用wait方法:永远不要在循环之外调用wait方法。循环会在等待之前和之后测试条件。
一个相关的话题是,为了唤醒等待的线程,你应该使用notify还是notifyAll,一个是唤醒单个正在等待的线程,另一个是唤醒所有正在等待的线程。一种常见的说法是,你总是应该使用notifyAll,这是合理而保守的建议。它总会产生正确的结果,因为它可以保证你将会唤醒所有需要被唤醒的线程,你可能也会唤醒其它一些线程,但是这不会影响程序的正确性,这些线程醒来之后,会检查它们正在等待的条件,如果发现条件并不满足,就会继续等待。同时还可以避免来自不相关线程的意外或恶意的等待,否则,这样的等待会吞掉一个关键的通知,使真正的接收线程无限的等待下去。
5. 线程安全性的文档化
如下的列表概括了线程安全性的几种级别:
(1)不可变的——这个类的实例是不变的,所以,不需要外部的同步。如String、Long、BigInteger等。
(2)无条件的线程安全——这个类的实例是可变的,但是这个类有着足够的内部同步,所以,它的实例可以被并发的使用,无需任何外部同步。如Random、ConcurrentHashMap等。
(3)有条件的线程安全——除了有些方法为进行安全的并发使用而需要外部同步之外,这种线程安全级别与无条件的线程安全相同。如Collections.synchronized包装返回的集合,它们的迭代器要求外部同步。
(4)非线程安全——这个类的实例是可变的。为了并发地使用它们,客户必须利用自己选择的外部同步包围每个方法调用。如ArrayList、HashMap等。
(5)线程对立的——这个类不能安全地被多个线程并发使用,即使所有的方法调用都被外部同步包围。线程对立的根源通常在于、没有同步地修改静态数据。这种类是因为没有考虑到并发性而产生的后果。如System.runFinalzersOnExit(已删除)
在文档中描述一个有条件的线程安全类要特别小心。你必须指明哪个调用序列需要外部同步,还要指明为了执行这些序列,必须获得哪一把锁。
当一个类承诺了“使用一个公有可访问的锁对象”时,就意味着允许客户端以原子的方式执行一个方法调用序列,但是,这种灵活性是要付出代价的。首先并发集合使用的那种并发控制,并不能与高性能的内部并发控制相兼容。然后客户客户端还可以发起拒绝服务攻击,他只需超时地保持公有可访问锁即可,这有可能是无意的,也可能是有意的。为了避免这种拒绝服务攻击,应该使用一个私有锁对象来代替同步的方法:
private final Object lock = new Object();
public void foo(){
synchronized(lock){... ... }
}
因为这个私有锁对象不能被这个类的客户端程序所访问,所以它们不可能妨碍对象的同步。注意lock域被声明为final的,这样可以防止不小心改变它的内容,而导致不同步访问包含对象的悲惨后果。
私有锁对象模式只能用在无条件的安全类上。有条件的线程安全类不能使用这种模式,因为在执行某些方法调用序列时,它们的客户端程序必须获得哪把锁。私有锁对象模式特别选用于那些专门为继承设计的类。总之,如果你编写的是无条件的线程安全类,就应该考虑使用私有锁对象来代替同步的方法。这样可以防止客户端程序和子类的不同步干扰,让你能够在后续的版本中灵活地对并发控制采用更加复杂的方法。
6. 慎用延迟初始化
延迟初始化是延迟到需要域的值时才将它初始化的这种行为。如果永远不需要这个值,这个域就永远不会被初始化。这种方法既选用于静态域,也选用于实例域。虽然延迟初始化主要是一种优化,但它也可以用来打破类和实例初始化的有富循环。和大多数优化一样,对于延迟初始化,最好的建议"除非绝对必要,否则就不要这么做"。延迟初始化如同一把双刃剑,它确实降低了实例对象创建的开销,却增加了访问被延迟初始化的域的开销。
如果域只在类的实例部分被访问,并且初始化这个域的开销很高,可能就值得进行延迟初始化。当有多个线程时,延迟初始化是需要技巧的,如果两个或者多个线程共享一个延迟初始化的域,采用某种形式的同步是很重要的,否则就可能造成严重的Bug。
如果利用延迟优化来破坏初始化的循环,就要使用同步访问方法,因为它是最简单、最清楚的替代方法:如下:
public class TestClass {
private FieldType field;
synchronized FieldType getField() {
if (field == null)
field = computeFieldValue();
return field;
}
}
如果出于性能的考虑而需要对静态域使用延迟初始化,可以考虑使用延迟初始化Holder class 模式:
public class TestClass {
private static class FieldHolder {
static final FieldType field = computeFieldValue();
}
static FieldType getField() {
return FieldHolder.field;
}
}
当getField()方法第一次被调用时,它第一次读取FieldHolder.field,导致FieldHolder 类得到初始化。这种模式的魅力在于,getField 方法没有被同步,并且只执行一个域访问,因此延迟初始化实际上并没有增加任何访问成本。现在的VM 将在初始化该类的时候,同步域的访问。一旦这个类被初始化,VM将修补代码,以便后续对该域的访问不会导致任何测试或者同步。
如果出于性能的考虑而需要对实例域使用延迟初始化,可使用双重检查模式:
public class TestClass {
private volatile FieldType f;
FieldType getField() {
FieldType result = f;
if (result == null) { //如果是数值型的基本类型域时,需用0来检查
synchronized(this) {
result = f;
if (result == null)
f = result = computeFieldValue();
}
}
return result;
}
}
注意在上面的代码中,首先将域字段f 声明为volatile 变量,其语义在之前的条目中已经给出解释,这里将不再赘述。再者就是在进入同步块之前,先针对该字段进行验证,如果不是null,即已经初始化,就直接返回该域字段,从而避免了不必要的同步开销。然而需要明确的是,在同步块内部的判断极其重要,因为在第一次判断之后和进入同步代码块之前存在一个时间窗口,而这一窗口则很有可能造成不同步的错误发生,因此第二次验证才是决定性的。
在该示例代码中,使用局部变量result 代替volatile 的域字段,可以避免在后面的访问中每次都从主存中获取数据,从而提高函数的运行性能。事实上,这只是一种代码优化的技巧而已。
如果需要对一个可以接受重复初始化实例域延迟初始化,可使用单重检查模式:
public class TestClass {
private volatile FieldType f;
FieldType getField() {
FieldType result = f;
if (result == null) //如果是数值型的基本类型域时,需用0来检查
f = result = computeFieldValue();
return result;
}
}
简而言之,大多数的域应该正常地进行初始化,而不是延迟初始化。如果为了达到性能目标,或者为了破坏有害的初始化循环,而必须延迟初始化一个域,就可以使用相应的延迟初始化方法。
7. 不要依赖于线程调度器
当有多个线程可以运行时,由线程调度器决定哪些线程将会运行,以及运行多长时间。任何一个合理的操作系统在做出这样的决定时,都会努力做到公正,但是所采用的策略却大相径庭。因此,编写良好的程序不应该依赖于这种策略的细节,任何依赖于线程调度器来达到正确性或者性能要求的程序,很有可能都是不可移植的。
要编写健壮的、响应良好的、可移植的多线程应用程序,最好的办法是确保可运行线程(等待的线程并不是可运行的)的平均数量不明显多于处理器的数量。这使得线程调度器没有更多的选择:它只需要运行这些可运行的线程,直到它们不再可运行为止。即使在根本不同的线程调度算法下,这些程序的行为也不会有很大的变化。
如果线程没有在做有意义的工作,就不应该运行。应适当地规定线程池的大小,并且使任务保持行当地小,彼此独立,任务也不应该大小,否则分配的开销也会影响性能。
线程不应该一直处于忙——等的状态,即反复地检查一个共享对象,以等待某些事情发生。除了使程序易受到调度器的变化影响之外,忙——等这种做法也会极大地增加处理器的负担,降低了同一机器上其他进程可以完成的有用的工作量。
如果某一程序不能工作,是因为某些线程无法像其他线程那样获得足够的CPU时间,那么,不要企图通过Thread.yield(它不做实质性的工作,只是将控制权返回给它的调用者)来修正该程序,这样得到的程序仍然是不可移植的,更好的解决办法是重新构造应用程序,以减少可并发运行的线程数量。
有一种相关的方法是调整线程的优先级,同样,线程优先级是Java平台上最不可移植的特征了。
8. 避免使用线程组
线程组的初衷是作为一种隔离applet(小程序)的机制,当然是出于安全的考虑,但是它们从来没有真正履行这个承诺。它们很少的用处是同时把Thread的某些基本功能应用到一组线程上,但很少使用。如果你正在设计的一个类需要处理线程的逻辑组,或许就应该使用线程池executor。
1. 同步访问共享的可变数据
2. 避免过度同步
3. executor和task优先干线程
4. 并发工具优先于wait和notify
5. 线程安全性的文档化
6. 慎用延迟初始化
7. 不要依赖于线程调度器
8. 避免使用线程组
1. 同步访问共享的可变数据
关键字synchronized可以保证在同一时刻,只有一个线程可以执行某一个方法,或者某一个代码块。对象同步并不仅限于当多个线程操作同一可变对象时,仍然能够保证该共享对象的状态始终保持一致。与此同时, 他还可以保证进入同步方法或者同步代码块的每个线程,都看到由同一个锁保护的之前所有的修改效果。
Java语言规范保证了读写一个变量是原子的,除非这个变量的类型为long 或double。换句话说, 读取一个非long 或double 类型的变量,可以保证返回的值是某个线程保存在该变量中的,即时多个线程在没有同步的情况下并发地修改这个变量也是如此。你可能听说过,为了提高性能,在读或写原子数据的时候,应该避免使用同步,这个建议是非常危险而错误的。虽然语言规范保证了线程在读取原子数据的时候,不会看到任意的数值,但是它并不保证一个线程写入的值对于另一个线程将是可见的。为了在线程之间进行可靠的通信,也为了互斥访问,同步是必要的。
即便这样做不会带来数据同步修改的问题,但是他会导致另外一个更为隐匿的错误发生。见如下代码:
public class StopThread {
private static boolean stopRequested = false;
public static void main(String[] args) throw InterruptedException
{
Thread bgThread = new Thread(new Runnable() {
public void run() {
int i = 0;
while (!stopRequested)
i++;
}
});
bgThread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}
对于上面的代码片段,有些人会认为在主函数sleep 一秒后,工作者线程的循环状态标志(stopRequested)就会被修改,从而致使工作者线程正常退出。然而事实却并非如此,因为Java 的规范中并没有保证在非同步状态下,一个线程修改的变量,在另一个线程中就会立即可见。事实上,这也是Java针对内存模型进行优化的一个技巧。没有同步,虚拟机将这个代码:
while (!stopRequested)
i++;
转换成这样:
if (!stopRequested) {
while (true)
i++;
}
这是可以接收的,这种优化被称为提升,正是HotSpot Server VM的工作。结果是个活性失败:这个程序无法前进。修正这个问题的一种方式是同步访问stopRequested域。见如下代码:
public class StopThread {
private static boolean stopRequested = false;
private static synchronized void requestStop() {
stopRequested = true;
}
private static synchronized boolean stopRequested() {
return stopRequested;
}
public static void main(String[] args) throw InterruptedException
{
Thread bgThread = new Thread(new Runnable() {
public void run() {
int i = 0;
while (!stopRequested())
i++;
}
});
bgThread.start();
TimeUnit.SECONDS.sleep(1);
requestStop();
}
}
在上面的修改代码中,读写该变量的函数均被加以同步。如果读和写方法没有都被同步,同步就不会起作用。
StopThread中被同步方法的动作即使没有同步也是原子的。换句话说,这些方法的同步只是为了它的通信效果,而不是为了互访访问。事实上,Java 中还提供了另外一种方式用于处理该类问题,即volatile 关键字。该单词的直译为“易变的”,引申到这里就是告诉cpu 该变量是容易被改变的变量,不能每次都从当前线程的内存模型中获取该变量的值,而是必须从主存中获取,这种做法所带来的唯一负面影响就是效率的折损,但是相比于
synchronized 关键字,其效率优势还是非常明显的。见如下代码:
public class StopThread {
private static volatile boolean stopRequested = false;
public static void main(String[] args) throw InterruptedException
{
Thread bgThread = new Thread(new Runnable() {
public void run() {
int i = 0;
while (!stopRequested)
i++;
}
});
bgThread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}
和第一个代码片段相比,这里只是在stopRequested 域变量声明之前加上volatile 关键字,从而保证该变量为易变变量。然而需要说明的是,该关键字并不能完全取代synchronized 同步方式,见如下代码:
public class Test {
private static volatile int nextID = 0;
public static int generateNextID() {
return nextID++;
}
}
generateNextID方法的用意为每次都给调用者生成不同的ID 值,遗憾的是,最终结果并不是我们期望的那样,当多个线程调用该方法时,极有可能出现重复的ID 值。问题在于增量操作符(++)不是原子的,而是由两个指令构成,首先是读取一个值,然后写回一个新值。由此可见,这两个指令之间的时间窗口极有可能造成数据的不一致。
修正generateNextID方法的一种方法是在它的声明中增加synchronized修饰符,并删除volatile修饰符。另一种方法是使用JDK(1.5 later)中java.util.concurrent.atomic包提供的AtomicLong类,使用该类性能要明显好于synchronized 的同步方式,见如下修复后的代码:
public class Test {
private static final AtomicLong nextID = new AtomicLong();
public static long generateNextID() {
return nextID.getAndIncrement();
}
}
避免本条目中所讨论的问题的最佳办法是不共享可变的数据,要么共享不可变的数据,要么压根不共享。
简而言之,当多个线程共享可变数据的时候,每个读或者写数据的线程都必须执行同步。如果没有同步,就无法保证一个线程所做的修改可以被另一个线程获知。如果只需要线程之间的交互通信,而需要互斥,volatile修饰符就是一种可以接受的同步形式。
2. 避免过度同步
过度同步所导致的最明显问题就是性能下降,特别是在如今的多核时代,再有就是可能引发的死锁和 一系列不确定性的问题。
当同步函数或同步代码块内调用了外来方法,如可被子类覆盖的方法,或外部类的接口方法等。由于这些方法的行为存在一定的未知性,如果在同步块内调用了类似的方法,将极有可能给当前的同步带来未知的破坏性。见如下代码:
public class ObservableSet<E> extends ForwardingSet<E> {
public ObservableSet(Set<E> set) {
super(set);
}
private final List<SetObserver<E>> observers = new ArrayList<SetObserver<E>>();
public void addObserver(SetObserver<E> observer) {
synchronized(observers) {
observers.add(observer);
}
}
public boolean removeObserver(SetObserver<E> observer) {
synchronized(observers) {
return observers.remover(observer);
}
}
private void notifyElementAdded(E element) {
synchronized(observers) {
for (SetObserver<E> observer : observers)
observer.added(this,element);
}
}
@Override public boolean add(E element) {
boolean added = super.add(element);
if (added)
notifyElementAdded(element);
return added;
}
@Override public boolean addAll(Collection<? extends E> c) {
boolean result = false;
for (E element : c)
result |= add(element);
return result;
}
}
Observer通过调用addObserver方法预定通知,通过调用removeObserver方法取消预定。在这两种情况下,这个回调接口的实例都会被传递给方法:
public interface SetObserver<E> {
void added(ObservableSet<E> set,E element);
}
如果只是粗略地检测一下,ObservableSet会显得很正常。如下:
public static void main(String[] args) {
ObservableSet<Integer> set = new ObservableSet<Integer>(new HashSet<Integer>());
set.addObserver(new SetObserver<Integer>() {
public void added(ObservableSet<Integer> s, Integer e) {
System.out.println(e);
}
});
for (int i = 0; i < 100; i++)
set.add(i);
}
对于这个测试用例,他完全没有问题,可以保证得到正确的输出,即打印出0-99 的数字。现在我们换一个观察者接口的实现方式,见如下代码片段:
set.addObserver(new SetObserver<Integer>() {
public void added(ObservableSet<Integer> s,Integer e) {
System.out.println(e);
if (e == 23)
s.removeObserver(this);
}
});
对于以上代码,你可能以为这个程序会打印出0~23的数字,之后观察者会取消预定,程序会悄悄地完成它的工作。实际上却是打印出0~23的数字,然后抛出ConcurrentModificationException 异常。问题在于,当notifyElementAdded调用观察者的added方法时,它正处于遍历observers列表的过程中,added方法调用可观察集合的removeObserver方法,从而调用observers.remove。现在有麻烦了,我们正在企图在遍历列表的过程中,将一个元素从列表中删除,这是非法的。notyfyElementAdded方法中的迭代是在一个同步的块中,可以防止并发的修改,但是无法防止迭代线程本身回调到可观察的集合中,也无法防止修改它的observers列表。
下面看另一个例子,编写一个试图取消预定的观察者,但是不直接调用removeObserver,它用另一个线程的服务来完成。这个观察者使用了一个executor service:
set.addObserver(new SetObserver<Integer>() {
public void added(final ObservableSet<Integer> s,Integer e) {
System.out.println(e);
if (e == 23)
ExecutorService executor = Executors.newSingleThreadExecutor();
final SetObserver<Integer> observer = this;
try{
executor.submit(new Runnable(){
public void run(){
s.removeObserver(observer);
}
}).get();
}catch(ExecutionException ex){
throw new AssertionError(ex.getCause());
}catch(InterruptedException ex){
throw new AssertionError(ex.getCause());
}finally{
executor.shutdown();
}
}
}
});
这一次我们没有遇到异常,而是遭遇了死锁。后台线程调用s.removeObserver,它企图锁定observers,但它无法获得该锁,因为主线程已经有锁了。在这期间,主线程一直等待后台线程来完成对观察者的删除,这正是造成死锁的原因。
由于Java中synchronized 关键字构成的锁是可重入的,或者说是可递归的,即在同一个线程内可多次调用且不会被阻塞,这种调用不会死锁,就像第一个例子,它会产生一个异常。如果恰恰相反,我们的冲突调用来自于多个线程,那么将会形成死锁。在多线程的应用程序中,死锁是一种比较难以重现和定位的错误。
为了解决上述问题,我们需要做的一是将调用外部代码的部分移出同步代码块,再有就是针对该遍历,我们需要提前copy 出来一份,并基于该对象进行遍历,从而避免了上面的并发访问冲突,如下:
private void notifyElementAdded(E element) {
List<SetObserver<E>> snapshot = null;
synchronized(observers) {
snapshot = new ArrayList<SetObserver<E>>(observers);
}
for (SetObserver<E> Observer : snapshot)
Observer.added(this,element);
}
事实上,还有一种更好的方法,Java1.5以来,Java类库就提供了一个并发集合,叫做CopyOnWriteArrayList,这是专门为此定制的。它通过重新拷贝整个底层数组,在这里实现所有的写操作。由于内部数组永远不改动,因此迭代不需要锁定,速度也非常快。修改如下:
public class ObservableSet<E> extends ForwardingSet<E> {
public ObservableSet(Set<E> set) {
super(set);
}
private final List<SetObserver<E>> observers = new CopyOnWriteArrayList<SetObserver<E>>();
... ...
上面的两个修改都避免了出现异常和死锁。
通常,你应该在同步区域内做尽可能少的工作。获得锁,检查共享数据,根据需要转换数据,然后放掉锁。如果你必须要执行某个耗时的动作,则应设法把这个动作移到同步区域的外面。
在这个多核的时代,过度同步的实际成本并不是指获取锁所花费的CPU时间,而是指失去了并行的机会,以及因为需要确保每个核都有一个一致的内存视图而导致的延迟。过度同步的另一项潜在开销在于,它会限制VM优化代码执行的能力。
如果一个可变的类要并发使用,应该使这个类变成是线程安全的,通过内部同步,你还可以获得明显比从外部锁定整个对象更高的并发性。否则,就不要在内部同步,让客户在必要的时候从外部同步。减少不必要的代码同步还可以大大提高程序的并发执行效率,一个非常明显的例子就是StringBuffer,该类在JDK 的早期版本中即以出现,是数据操作同步类,即时我们是以单线程方式调用该类的方法,也不得不承受块同步带来的额外开销。Java 在1.5 中提供了非同步版本的StringBuilder 类,这样在单线程应用中可以消除因同步而带来的额外开销,对于多线程程序,可以继续选择StringBuffer,或者在自己认为需要同步的代码部分加同步块。 所以,当你不确定的时候,就不要同步你的类,而是应该建立文档,注明它不是线程安全的。
简而言之,为了避免死锁和数据破坏,千万不要从同步区域内部调用外来方法。更为一般的讲,要尽量的限制同步区域内部的工作量。当你在设计一个可变类的时候,要考虑一下它们是否应该自己完成同步操作。在现在这个多核时代,这比永远不要过度同步来得更重要。只有当你有足够的理由一定要在内部同步类的时候,才应该这么做,同时还应该将这个决定清楚地写到文档中。
3. executor和task优先于线程
在Java 1.5 中提供了java.util.concurrent,在这个包中包含了Executor Framework 框架, 这是一个很灵活的基于接口的任务执行工具。该框架提供了非常方便的调用方式和强大的功能,如:
ExecutorService executor = Executors.newSingleThreadExecutor(); //创建一个单线程执行器对象。
executor.execute(runnable); //提交一个待执行的任务。
executor.shutdown(); //使执行器优雅的终止。
你可以利用executor service完成更多的事情,如:可以等待完成一项目特殊的任务,可以等待executor service优雅地完成终止,可以在任务完成时逐个获取这些任务的结果,等等。
如果想让不止一个线程来处理来自这个队列的请求,只要调用一个不同的静态工厂,这个工厂创建了一种不同的executor service,称作线程池。你可以用固定或者可变数目的线程创建一个线程池。java.util.concurrent.Executors类包含了静态工厂,能为你提供所需的大多数executor。然而,如果你想要来点特别的,可以直接使用ThreadPoolExecutor类。这个类允许你控制线程池操作的几乎每个方面。
为特殊的应用程序选择executor service是很有技巧的。如果编写的是小程序,或者是轻载的服务器,使用Executors.newCachedThreadPool()通常是个不错的选择,因为它不需要配置,并且在一般情况下能够正确地完成工作。但是对于大负载的服务器来说,缓存的线程池就不是很好的选择了,在缓存的线程池中,被提交的任务没有排成队列,而是直接交给线程执行。如果没有线程可用,就创建一个新的线程。如果服务器负载得太重,以致它所有的CPU都全被占用了,当有更多的任务时,就会创建更多的线程,这样只会使情况变得更糟。对于该种情况,Executors 提供了另外一个工厂方法Executors.newFixedThreadPool(),它为你提供了一个包含固定线程数目的线程池,或者为了最大限度地控制它,就直接使用ThreadPoolExecutor类。
Executor Framework也有一个可以代替java.util.Timer的东西,即ScheduledThreadPoolExecutor,通过工厂方法Executors.ScheduledThreadPool()可以创建该类。虽然timer使用起来更加容易,但是被调用的线程池executor更加灵活。timer只用一个线程来执行任务,这在面对长期运行的任务时,会影响到定时的准确性。如果timer唯一的线程抛出未被捕获的异常,timer就会停止执行。被调度的线程池executor支持多个线程,并且优雅地从抛出未受检异常的任务中恢复。
4. 并发工具优先于wait和notify
自从Java1.5发行版本开始,Java平台就提供了更高级的并发工具,它们可以完成以前必须在wait和notify上手写代码来完成的各项工作。既然正确地使用wait和notify比较困难,就应该用更高级的并发工具来代替。
java.util.concurrent 中更高级的工具分成三类:Executor Framework(在3中简单说明)、并发集合(Concurrent Collection)以及同步器(Synchronizer)。
并发集合为标准的集合接口提供了高性能的并发实现。为了提高并发性,这些实现在内部管理同步,并发集合不可能排除并发活动,将它锁定没有什么作用,只会使程序的速度变慢。
有些集合接口已经通过依赖状态的修改操作进行了扩展,将几个基本操作合并到了单个原子操作中。如java.util.concurrent 中提供的并发集合就有更好的并发性,其性能通常数倍于普通集合。如ConcurrentHashMap,它扩展了Map接口,并添加了几个方法,包括putIfAbsent(key, value),当键没有映射时会替它插入一个映射,并返回与键关联的前一个值,如果没有这样的值,则返回null,ConcurrentHashMap除了提供卓越的并发性之外,速度也非常快。换句话说,除非有极其特殊的原因存在,否则在并发的情况下,一定要优先选择ConcurrentHashMap,而不是Collections.syschronizedmap 或者Hashtable。
java.util.concurrent 包中还提供了阻塞队列,它们会一直等待到可以成功执行为止。如BlockingQueue扩展了Queue接口,并添加了包括take在内的几个方法,它从队列中删除并返回了头元素,如果队列为空就等待。这样就允许将阻塞队列用于工作队列,也称作生产者-消费者队列,大多数ExecutorService实现都使用BlockingQueue,该队列极大的简化了生产者线程和消费者线程模型的编码工作。
同步器是一些使线程能够等待另一个线程的对象,允许它们协调动作。包括常用的CountDownLatch(倒计数锁存器)和Semaphore,和不常用的CyclicBarrier 和Exchanger。
CountDownLatch是一次性的障碍,允许一个或者多个线程等待一个或者多个线程来做某些事情。CountDownLatch 的唯一构造函数带有一个int 类型的参数,这个int 参数是指允许所有在等待的线程被处理之前,必须在锁存器上调用countDown 方法的次数。
现在我们给出一个简单应用场景,然后再给出用CountDownLatch 实现该场景的实际代码。场景描述如下:假设想要构建一个简单的框架,用来给一个动作的并发执行定时。这个框架中包含单个方法,这个方法带有一个执行该动作的executor,一个并发级别(表示要并发执行该动作的次数),以及表示该动作的runnable。所有的工作线程自身都准备好,要在timer 线程启动时钟之前运行该动作。当最后一个工作线程准备好运行该动作时,timer 线程就开始执行,同时允许工作线程执行该动作。一旦最后一个工作线程执行完该动作,timer 线程就立即停止计时。直接在wait 和notify 之上实现这个逻辑至少来说会很混乱,而在CountDownLatch 之上实现则相当简单。见如下示例代码:
public static long time(Executor executor,int concurrency,final Runnable action) {
final CountDownLatch ready = new CountDownLatch(concurrency);
final CountDownLatch start = new CountDownLatch(1);
final CountDownLatch done = new CountDownLatch(concurrency);
for (int i = 0; i < concurrency; i++) {
executor.execute(new Runnable() {
public void run() {
ready.countDown();
try {
start.await();
action.run();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
done.countDown();
}
}
});
}
//等待工作者线程准备可以执行,即所有的工作线程均调用ready.countDown()方法。
ready.await();
//这里使用nanoTime,是因为其精确度高于System.currentTimeMills(),且不受系统的实时时钟的调整所影响。
long startNanos = System.nanoTime();
//该语句执行后,工作者线程中的start.await()均将被唤醒。
start.countDown();
//下面的等待,只有在所有的工作者线程均调用done.countDown()之后才会被唤醒。
done.await();
return System.nanoTime() - startNanos;
}
注意这个方法使用了三个倒计数锁存器,第一个是ready,工作线程用它来告诉timer线程它们已经准备好了。然后工作线程在第二个锁存上等待,也就是start。当最后一个工作线程调用ready.countDown时,timer线程记录下起始时间,并调用start.countDown,允许所有的工作线程继续进行。然后timer线程在第三个锁存器上等待,直到最后一个工作线程运行完该动作,并调用done.countDown。一旦调用这个,timer线程就会苏醒过来,并记录下结束的时间。
虽然你始终应该优先使用并发工具,而不是使用wait和notify,但可能必须维护使用了wait和notify的遗留代码。wait方法被用来使线程等待某个条件,它必须在同步区域内部被调用,这个同步区域将对象锁定在了调用wait方法的对象上。下面是使用wait方法的标准模式:
synchronized(obj){
while(<condition does not hold>)
obj.wait()
... ...
}
始终应该使用wait循环模式调用wait方法:永远不要在循环之外调用wait方法。循环会在等待之前和之后测试条件。
一个相关的话题是,为了唤醒等待的线程,你应该使用notify还是notifyAll,一个是唤醒单个正在等待的线程,另一个是唤醒所有正在等待的线程。一种常见的说法是,你总是应该使用notifyAll,这是合理而保守的建议。它总会产生正确的结果,因为它可以保证你将会唤醒所有需要被唤醒的线程,你可能也会唤醒其它一些线程,但是这不会影响程序的正确性,这些线程醒来之后,会检查它们正在等待的条件,如果发现条件并不满足,就会继续等待。同时还可以避免来自不相关线程的意外或恶意的等待,否则,这样的等待会吞掉一个关键的通知,使真正的接收线程无限的等待下去。
5. 线程安全性的文档化
如下的列表概括了线程安全性的几种级别:
(1)不可变的——这个类的实例是不变的,所以,不需要外部的同步。如String、Long、BigInteger等。
(2)无条件的线程安全——这个类的实例是可变的,但是这个类有着足够的内部同步,所以,它的实例可以被并发的使用,无需任何外部同步。如Random、ConcurrentHashMap等。
(3)有条件的线程安全——除了有些方法为进行安全的并发使用而需要外部同步之外,这种线程安全级别与无条件的线程安全相同。如Collections.synchronized包装返回的集合,它们的迭代器要求外部同步。
(4)非线程安全——这个类的实例是可变的。为了并发地使用它们,客户必须利用自己选择的外部同步包围每个方法调用。如ArrayList、HashMap等。
(5)线程对立的——这个类不能安全地被多个线程并发使用,即使所有的方法调用都被外部同步包围。线程对立的根源通常在于、没有同步地修改静态数据。这种类是因为没有考虑到并发性而产生的后果。如System.runFinalzersOnExit(已删除)
在文档中描述一个有条件的线程安全类要特别小心。你必须指明哪个调用序列需要外部同步,还要指明为了执行这些序列,必须获得哪一把锁。
当一个类承诺了“使用一个公有可访问的锁对象”时,就意味着允许客户端以原子的方式执行一个方法调用序列,但是,这种灵活性是要付出代价的。首先并发集合使用的那种并发控制,并不能与高性能的内部并发控制相兼容。然后客户客户端还可以发起拒绝服务攻击,他只需超时地保持公有可访问锁即可,这有可能是无意的,也可能是有意的。为了避免这种拒绝服务攻击,应该使用一个私有锁对象来代替同步的方法:
private final Object lock = new Object();
public void foo(){
synchronized(lock){... ... }
}
因为这个私有锁对象不能被这个类的客户端程序所访问,所以它们不可能妨碍对象的同步。注意lock域被声明为final的,这样可以防止不小心改变它的内容,而导致不同步访问包含对象的悲惨后果。
私有锁对象模式只能用在无条件的安全类上。有条件的线程安全类不能使用这种模式,因为在执行某些方法调用序列时,它们的客户端程序必须获得哪把锁。私有锁对象模式特别选用于那些专门为继承设计的类。总之,如果你编写的是无条件的线程安全类,就应该考虑使用私有锁对象来代替同步的方法。这样可以防止客户端程序和子类的不同步干扰,让你能够在后续的版本中灵活地对并发控制采用更加复杂的方法。
6. 慎用延迟初始化
延迟初始化是延迟到需要域的值时才将它初始化的这种行为。如果永远不需要这个值,这个域就永远不会被初始化。这种方法既选用于静态域,也选用于实例域。虽然延迟初始化主要是一种优化,但它也可以用来打破类和实例初始化的有富循环。和大多数优化一样,对于延迟初始化,最好的建议"除非绝对必要,否则就不要这么做"。延迟初始化如同一把双刃剑,它确实降低了实例对象创建的开销,却增加了访问被延迟初始化的域的开销。
如果域只在类的实例部分被访问,并且初始化这个域的开销很高,可能就值得进行延迟初始化。当有多个线程时,延迟初始化是需要技巧的,如果两个或者多个线程共享一个延迟初始化的域,采用某种形式的同步是很重要的,否则就可能造成严重的Bug。
如果利用延迟优化来破坏初始化的循环,就要使用同步访问方法,因为它是最简单、最清楚的替代方法:如下:
public class TestClass {
private FieldType field;
synchronized FieldType getField() {
if (field == null)
field = computeFieldValue();
return field;
}
}
如果出于性能的考虑而需要对静态域使用延迟初始化,可以考虑使用延迟初始化Holder class 模式:
public class TestClass {
private static class FieldHolder {
static final FieldType field = computeFieldValue();
}
static FieldType getField() {
return FieldHolder.field;
}
}
当getField()方法第一次被调用时,它第一次读取FieldHolder.field,导致FieldHolder 类得到初始化。这种模式的魅力在于,getField 方法没有被同步,并且只执行一个域访问,因此延迟初始化实际上并没有增加任何访问成本。现在的VM 将在初始化该类的时候,同步域的访问。一旦这个类被初始化,VM将修补代码,以便后续对该域的访问不会导致任何测试或者同步。
如果出于性能的考虑而需要对实例域使用延迟初始化,可使用双重检查模式:
public class TestClass {
private volatile FieldType f;
FieldType getField() {
FieldType result = f;
if (result == null) { //如果是数值型的基本类型域时,需用0来检查
synchronized(this) {
result = f;
if (result == null)
f = result = computeFieldValue();
}
}
return result;
}
}
注意在上面的代码中,首先将域字段f 声明为volatile 变量,其语义在之前的条目中已经给出解释,这里将不再赘述。再者就是在进入同步块之前,先针对该字段进行验证,如果不是null,即已经初始化,就直接返回该域字段,从而避免了不必要的同步开销。然而需要明确的是,在同步块内部的判断极其重要,因为在第一次判断之后和进入同步代码块之前存在一个时间窗口,而这一窗口则很有可能造成不同步的错误发生,因此第二次验证才是决定性的。
在该示例代码中,使用局部变量result 代替volatile 的域字段,可以避免在后面的访问中每次都从主存中获取数据,从而提高函数的运行性能。事实上,这只是一种代码优化的技巧而已。
如果需要对一个可以接受重复初始化实例域延迟初始化,可使用单重检查模式:
public class TestClass {
private volatile FieldType f;
FieldType getField() {
FieldType result = f;
if (result == null) //如果是数值型的基本类型域时,需用0来检查
f = result = computeFieldValue();
return result;
}
}
简而言之,大多数的域应该正常地进行初始化,而不是延迟初始化。如果为了达到性能目标,或者为了破坏有害的初始化循环,而必须延迟初始化一个域,就可以使用相应的延迟初始化方法。
7. 不要依赖于线程调度器
当有多个线程可以运行时,由线程调度器决定哪些线程将会运行,以及运行多长时间。任何一个合理的操作系统在做出这样的决定时,都会努力做到公正,但是所采用的策略却大相径庭。因此,编写良好的程序不应该依赖于这种策略的细节,任何依赖于线程调度器来达到正确性或者性能要求的程序,很有可能都是不可移植的。
要编写健壮的、响应良好的、可移植的多线程应用程序,最好的办法是确保可运行线程(等待的线程并不是可运行的)的平均数量不明显多于处理器的数量。这使得线程调度器没有更多的选择:它只需要运行这些可运行的线程,直到它们不再可运行为止。即使在根本不同的线程调度算法下,这些程序的行为也不会有很大的变化。
如果线程没有在做有意义的工作,就不应该运行。应适当地规定线程池的大小,并且使任务保持行当地小,彼此独立,任务也不应该大小,否则分配的开销也会影响性能。
线程不应该一直处于忙——等的状态,即反复地检查一个共享对象,以等待某些事情发生。除了使程序易受到调度器的变化影响之外,忙——等这种做法也会极大地增加处理器的负担,降低了同一机器上其他进程可以完成的有用的工作量。
如果某一程序不能工作,是因为某些线程无法像其他线程那样获得足够的CPU时间,那么,不要企图通过Thread.yield(它不做实质性的工作,只是将控制权返回给它的调用者)来修正该程序,这样得到的程序仍然是不可移植的,更好的解决办法是重新构造应用程序,以减少可并发运行的线程数量。
有一种相关的方法是调整线程的优先级,同样,线程优先级是Java平台上最不可移植的特征了。
8. 避免使用线程组
线程组的初衷是作为一种隔离applet(小程序)的机制,当然是出于安全的考虑,但是它们从来没有真正履行这个承诺。它们很少的用处是同时把Thread的某些基本功能应用到一组线程上,但很少使用。如果你正在设计的一个类需要处理线程的逻辑组,或许就应该使用线程池executor。
发表评论
-
EffectiveJava--序列化
2014-12-03 11:43 1516本章内容: 1. 谨慎地实 ... -
EffectiveJava--异常
2014-11-23 21:47 1080本章内容: 1. 只针对异 ... -
EffectiveJava--通用程序设计
2014-11-20 16:22 777本章内容: 1. 将局部变 ... -
EffectiveJava--方法
2014-11-16 17:44 1341本章内容: 1. 检查参数的有效性 2. 必要时进行保护性拷贝 ... -
EffectiveJava--枚举和注解
2014-11-15 21:23 1638本章内容: 1. 用enum代 ... -
EffectiveJava--泛型
2014-11-08 00:22 1209本章内容: 1. 请不要在 ... -
EffectiveJava--类和接口
2014-11-05 00:30 1383本章内容: 1. 使类和成 ... -
EffectiveJava--对象通用方法
2014-10-27 22:49 918本章内容: 1. 覆盖equals ... -
EffectiveJava--创建和销毁对象
2014-10-22 23:38 959本章内容: 1. 考虑用静 ... -
Java多线程
2014-04-28 18:56 1036一、多线程简介 对于Java而言,可以在一个程序中并发地启 ... -
Java异常处理
2014-04-27 00:49 1438一、异常简介 在程序运行过程中,如果环境检测出一个不可以执 ... -
Java集合框架
2014-04-24 23:56 1861一、集合框架简介 数据结构是以某种形式将数据组织在一起的集 ... -
Java编程规范整理
2014-03-29 21:36 1560一、排版1. 代码采用缩进风格编写,缩进空格数为4,不允许 ...
相关推荐
《Effective Java》是Java编程领域的一本经典著作,由Joshua Bloch撰写,第二版发布于2008年。这本书旨在提供实用的编程指导,帮助开发者写出更高效、更可维护的Java代码。以下是对书中核心知识点的详细解读: 1. *...
讨论Java并发工具和最佳实践,如何编写线程安全的代码,以及避免并发问题。 ### 第九章 序列化 解释如何有效地实现序列化,以及序列化可能带来的安全和性能问题。 ### 第十章 注解 介绍注解的使用,自定义注解的...
java逻辑思维笔试题Effective Java - 第三版笔记 章节索引 02 - 创建和销毁对象 03 - 所有对象通用的方法 04 - 类和接口 05 - 泛型 06 - 枚举和注释 07 - Lambda 和流 08 - 方法 09 - 通用编程 10 - 例外 11 - 并发 ...
- **语法简洁**:Go语言的语法设计简洁明了,避免了C++或Java中的复杂特性,如指针运算、模板等,使得代码更易读、易写。 - **结构体与方法**:Go语言中,通过定义结构体来创建自定义类型,并可以为结构体添加方法...
在编程领域,特别是Java开发中,"Effective Java"是一本非常经典的书籍,由Joshua Bloch撰写,书中提出了一系列最佳实践和设计原则,以帮助开发者编写出更高效、更安全的代码。根据提供的标题和描述,我们将探讨三个...
10. **并发编程**:虽然《Effective Java》并未深入讨论并发,但书中的一些原则,如Item 42 "避免线程局部变量" 提供了处理多线程问题的基本思路。 通过研究`Effective-Java-Concepts-master`这个压缩包中的源代码...
通过学习《Effective Go》,开发者可以掌握Go语言的最佳实践,写出更高质量的Go代码,充分利用其并发特性和简洁的语法,提高开发效率和代码可维护性。无论是初学者还是经验丰富的开发者,这本书都能提供宝贵的指导。
《Effective Java》是一本经典Java编程指南,作者是Joshua Bloch,这本书深入探讨了如何编写高质量、高效、可维护的Java代码。以下是对压缩包中各章节主要知识点的详细阐述: 1. **第2章 创建和销毁对象** - 单例...
"Effective Java 读书分享" 《Effective Java》读书分享.pptx 是一本 Java 编程语言指南,旨在帮助开发者编写高质量、可维护的 Java 代码。该书包含 90 个条目,每个条目讨论一条规则,涵盖了 Java 编程语言的...
### Java并发编程实战知识点概述 #### 一、Java并发特性详解 在《Java并发编程实战》这本书中,作者深入浅出地介绍了Java 5.0和Java 6中新增的并发特性。这些特性旨在帮助开发者更高效、安全地编写多线程程序。书中...
JAVA并发编程实践中文版 英文版 原书源码 带书签 java_concurrency_in_practice.pdf 英文版还是不错的,但是中文版的译者典型的没有技术功底,介绍上说什么专家, 翻译的非常差劲,有些句子都不通顺,都不知道自己去...
目录:一、创建和销毁对象 (1 ~ 7)二、对于所有对象都通用的方法 (8 ~ 12)三、类和接口 (13 ~ 22)四、泛型 (23 ~ 29)五、枚举和注解 (30 ~ 37)六、方法 ...65)九、并发 (66 ~ 73)十、序列化 (74 ~ 78)
《Effective Java》是Java开发领域的经典著作,由Joshua Bloch撰写,中文版第二版更是深受广大Java开发者喜爱。这本书提供了许多实用的编程实践和经验教训,帮助开发者编写出更高效、可维护的Java代码。这里我们将...
《Effective Java》是Java编程领域的一本经典著作,由Joshua Bloch撰写,它提供了许多实用的编程指导原则,帮助开发者写出更高效、更可维护的代码。这本书分为多个条目,每个条目都深入探讨了一个特定的Java编程实践...
《JAVA并发编程实践》随着多核处理器的普及,使用并发成为构建高性能应用程序的关键。Java 5以及6在开发并发程序中取得了显著的进步,提高了Java虚拟机的性能以及并发类的可伸缩性,并加入了丰富的新并发构建块。在...
### Java并发实践知识点详解 #### 一、书籍基本信息与作者介绍 - **书名**:《Java并发实践》(Java Concurrency in Practice) - **作者**:Brian Goetz, Tim Peierls, Joshua Bloch, Joseph Bowbeer, David ...
其内容涵盖了Java编程的多个方面,包括设计模式、并发处理、异常处理、类库使用等,为程序员提供了实用的指导原则和最佳实践。 在Java的世界里,"Effective"系列书籍以其深入浅出的讲解和实用的编程技巧深受好评。...
《Effective Enterprise Java》是一本深度探讨企业级Java应用开发的经典著作。这本书主要针对J2EE(Java 2 Platform, Enterprise Edition)平台,旨在提供一系列实用的编程指导和最佳实践,帮助开发者编写出高效、...
《Effective Java》是Java开发领域的经典著作,由Joshua Bloch编写,中文版第三版的出版为Java开发者提供了深入理解语言特性和最佳实践的机会。这本书包含了一系列编程实践和设计模式,旨在帮助程序员写出更高效、更...