`
sungang_1120
  • 浏览: 322449 次
  • 性别: Icon_minigender_1
  • 来自: 成都
社区版块
存档分类

Effective Java学习(并发)之——避免过度使用同步

阅读更多

      同步可以给我们在实际开发中带来很多的好处,合理的使用同步,将会更好的处理多线程及并发时数据的共享和一致性。但是,一句情况的不同,过度的使用同步可能会导致性能减低、死锁、甚至不确定的行为。

 

      为了避免活性失败和安全性失败,在一个被同步的方法或者代码块中,永远不要放弃对客户端的控制。换句话说,在一个被同步的区域内部,不要调用设计成被覆盖的方法,或者是由客户端以函数的形式提供的方法。从包含该同步区域的类的角度来看,这样的方法时外来的。这个类不知道该方法会做什么事情,也无法控制它。根据外来方法的作用,从同步区域中调用它会导致异常,死锁或者数据损坏。

 

      为了对这个过程进行具体的说明,来考虑下面这个类,他实现了一个可以观察到的集合包装。该类允许客户端在将元素添加到集合中时预定通知。这就是观察者模式。为了简洁起见,类在从集合中删除元素时没有提供通知,但是要提供通知也是件很容易的事情:

 

import java.util.*;


public class ForwardingSet<E> implements Set<E>{
	private final Set<E> s;
	
	public ForwardingSet(Set<E> s){
		this.s = s;
	}

	@Override
	public int size() {return this.s.size();}
	
	@Override
	public void clear() {this.s.clear();}
	
	@Override
	public boolean isEmpty() {return this.s.isEmpty();}

	@Override
	public boolean contains(Object o) {return this.s.contains(o);}

	@Override
	public Iterator<E> iterator() {return this.s.iterator();}

	@Override
	public Object[] toArray() {return this.s.toArray();}

	@Override
	public <T> T[] toArray(T[] a) {return this.s.toArray(a);}

	@Override
	public boolean add(E e) {return this.s.add(e);}

	@Override
	public boolean remove(Object o) {return this.s.remove(o);}

	@Override
	public boolean containsAll(Collection<?> c) {return this.s.containsAll(c);}

	@Override
	public boolean addAll(Collection<? extends E> c) {return this.s.addAll(c);}

	@Override
	public boolean retainAll(Collection<?> c) {return this.s.removeAll(c);}

	@Override
	public boolean removeAll(Collection<?> c) {return this.s.retainAll(c);}
} 

 

 

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;

public class ObservableSet<E> extends ForwardingSet<E>{

	public ObservableSet(Set<E> s) {
		super(s);
	}
	
	
	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.remove(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方法预定通知,通过调用removeObserve方法取消预定。在这两种情况下,这个回调接口实例会被传递给方法:

 

public interface SetObserver<E> {
	void added(ObservableSet<E> set, E element);
}

 如果粗略的检验一下,ObserverSet会显得很正常。例如,下面的程序打印0-99的数字:

 

 

public static void main(String[] args) {
		ObservableSet<Integer> set = new ObservableSet<Integer>(new HashSet<Integer>());
		set.addObserver(new SetObserver<Integer>() {
			
			@Override
			public void added(ObservableSet<Integer> set, Integer element) {
				System.out.print(element+" ");
			}
		});
		
		for(int i = 0; i < 100; i++){
			set.add(i);
		}
	}

      现在我们来尝试一些更复杂的例子。假设我们用一个addObserver调用来代替这个调用,用来替换的那个addObserver调用传递了一个打印Integer值得观察者,这个值被添加到了该集合中,如果值为23,这个观察者要将自身删除:

 

 

set.addObserver(new SetObserver<Integer>() {
			
			@Override
			public void added(ObservableSet<Integer> set, Integer element) {
				System.out.print(element+" ");
				if (element == 23) {
					set.removeObserver(this);
				}
			}
		});

      你可能以为这个程序会打印0-23的数字,之后观察者会取消预订,程序会悄悄的完成它的工作。实际上确实打印出0-23的数字然后抛出异常ConcurrentModificationException。问题在于,当notifyElementAdded调用观察者的added方法时,他正处于遍历objservers列表的过程中。added方法调用可观察集合的removeObserver方法,从而调用observers.remove方法。现在我们有麻烦了。我们正企图在遍历列表的过程中,将一个元素从列表中删除,这是非法的,notifyElementAdded方法中的迭代式在一个同步块中,可以防止并发修改,但是无法防止迭代线程本身回调到可观察的集合中,也无法防止修改它的observers列表。

 

 

     现在我们要尝试一些比较奇特的例子:我们来编写一个试图取消预定的观察者,但是不直接调用removeObserver,它用另一个线程的服务来完成,。这个观察者使用了一个executor service:

 

set.addObserver(new SetObserver<Integer>() {
			
			@Override
			public void added(final ObservableSet<Integer> set, Integer element) {
				System.out.print(element+" ");
				if (element == 23) {
					ExecutorService executorService = Executors.newSingleThreadExecutor();
					
					final SetObserver<Integer> observer = this;
					try {
						executorService.submit(new Runnable() {
							@Override
							public void run() {
								set.removeObserver(observer);
							}
						}).get();
					} catch (ExecutionException ex) {
						throw new AssertionError(ex.getCause());
					}catch (InterruptedException ex) {
						throw new AssertionError(ex.getCause());
					}finally{
						executorService.shutdown();
					}
				}
			}
		});

      这一次我们没有遇到异常,而是遭到死锁。后台线程调用set.removeObserver,他企图锁定observers,但他无法获得该锁,因为主线程已经没有锁了。在这期间,主线程一直在等待后台程序来完成对观察者的删除,这正是造成死锁的原因。

 

 

      这个例子是可以编写示范的,因为观察者实际上没有理理由使用后台线程,但是这个问题却是真实的。从同步区域调外来方法,在真实的系统中已经造成了许多死锁,例如GUI工具箱。

 

      在前面这两个例子中(异常和死锁),我们都还算幸运的。调用外来方法时,同步区域所保护的资源处于一致状态。假设档同步区域所保护的约束条件暂时无效时,你要从同步区域中调用一个外来方法。由于java程序设计语言的锁是可重入的,这种调用不会死锁。就像在第一个例子中一样,他会产生一个异常,因为调用线程已经有这个锁了,因此当该线程试图再次获得该锁时会成功,尽管概念上不相关的另一项操作正在该锁所保护的数据上进行着。这种失败的后果可能是灾难性的。从本质上来说,这个锁没有尽到他的职责。可再人的锁简化了多线程的面向对象程序的构造,但是他们可能hi将活性失败变成安全性失败。

 

      幸运的是,通过将外来方法的调用移出同步代码块来解决这个问题通常并不太难,对于notofyElementAdded方法,这还设计给observers列表拍张“快照“,然后没有锁也可以安全的遍历这个列表了,进过这一修改,前面两个例子运行起来便在也不会出现异常或者死锁了:

 

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,这是专门为此定制的。这是ArrayList的一种变体,通过重新拷贝整个底层数组,在这里实现所有的写操作。由于内部数组永远不会改动,因此迭代不需要锁定,速度也非常快。如果大量的使用,CopyOnWriteArrayList的性能将大受影响,但是对于观察者列表来说却是很好的,因为他们几乎不改动,并且经常遍历。

 

 

      如果这个列表改成使用CopyOnWriteArrayList,就不必改动ObservableSet的add和addAll方法,下面是这个类的其余代码。注意其中并没有任何显示的同步。

 

private final List<SetObserver<E>> observers = new CopyOnWriteArrayList<SetObserver<E>>();
	
	public void addObserver(SetObserver<E> observer) {
		observers.add(observer);
	}

	public boolean removeObserver(SetObserver<E> observer) {
		return observers.remove(observer);
	}

	private void notifyElementAdded(E element) {
		for (SetObserver<E> observer : observers) {
				observer.added(this, element);
		}
}

      在同步区域之外被调用的外来方法被称作:”开放调用”。除了可以避免死锁之外,开放调用还可以极大的增加并发性。外来方法的运行时间可能会任意长。如果在同步区域内调用外来方法,其他线程对受保护资源的访问就会遭受不必要的拒绝。

 

 

      通常,你应该在同步区域内做尽可能少的工作,。获得锁,检查共享数据,根据需要转换数据,然后放掉锁。如果你必须要执行某个很耗时的动作,则应该设法把这个动作移到同步区域的外面。

 

本篇的第一部分是关于正确性的。接下来,我们要简洁的讨论下性能。虽然自从java平台早期以来,同步的成本已经下降了,但更重要的是,永远不要过度同步,在这个多核的时代,过度同步的实际成本并不是指获取死锁所花费的CPU时间,而是指失去了并行的机会,以及因为需要确保每个核都有一个一致的内存视图而导致的延迟。过度同步另一个潜在的开销在于,他会限制VM优化代码执行的能力。

 

      如果一个可变的类要并发使用,应该使这个类编程线程安全的,通过内部同步,你还可以获得明显比从外部锁定整个对象更高的并发性。否则,就不要在内部同步。让客户在必要的时候从外部同步。在java平台出现的早期,许多类都违背了这个指导方针。例如,StringBuffer实例几乎总是被用于单个线程中,而他们执行的却是内部同步。为此,StringBuffer基本上都有StringBuilder代替,他在java1.5中是个非同步的StringBuffer。当你不确定的时候,就不要同步你的类,而是应该建立文档,说明他不是线程安全的。

 

      如果你在内部同步了类,就可以使用不同的方法来实现高并发性,例如分拆锁、分离锁和非阻锁并发控制。

 

      如果方法修改了静态域,俺么你也必须同步这个域的访问,即使他往往只用于单个线程。客户要求在这种方法上执行外部同步时不可能的,因为不可能保证其他不相关的客户也会执行外部同步,

 

      简而言之,为了避免死锁和数据破坏,千万不要从同步区域内调用外来的方法。更为一般的将,要尽量限制同步区域内部的工作量。当你设计一个可变的类的时候,要考虑一下他们是否应该自己完成同步操作。在现在这个多核的时代,这比永远不要多度同步来的更重要,只有当你有足够的理由一定要在内部同步类的时候,才可以这样做,同时还应该将这个决定清楚的写在文档中。

 

 

 

分享到:
评论

相关推荐

    Effective Java读书笔记.pdf

    Effective Java是一本关于Java编程语言的经典书籍,本笔记主要总结了Java语言的发展历程、静态工厂方法的应用、构造器模式的使用等重要知识点。 一、Java语言的发展历程 Java语言的发展可追溯到1991年,当时由...

    Effective Java第三版1

    《Effective Java》是Java编程领域的一本经典著作,由Joshua Bloch撰写,该书的第三版继续提供了关于如何编写高效、优雅、可维护的Java代码的指导。以下是基于给出的目录和部分内容提取的一些关键知识点: ### 第一...

    java设计模式之——策略模式

    在阅读《Effective Java》等编程书籍时,会发现作者常常推荐使用策略模式来代替使用重载方法或枚举类型,因为这提供了更好的灵活性和可扩展性。 总结一下,策略模式是设计模式中的重要一环,它通过将算法封装在独立...

    java并发编程实战(英文版)

    综上所述,《Java并发编程实战》不仅涵盖了Java并发编程的基础知识和技术细节,还包含了丰富的实践经验和前瞻性的思考,是任何一位从事Java开发工作的程序员不可或缺的学习资源。无论是初学者还是有经验的开发者都能...

    effective-java.pdf

    不可实例化的类使用静态工厂方法来提供方法的实现,如java.util.Collections类提供的不可修改、同步的集合等。不可变类使用静态工厂方法可以预先构建实例,避免重复创建等价对象。 5. 静态工厂方法与接口的结合 在...

    Effective.Enterprise.Java.中文版 高清pdf 下载

    《Effective Enterprise Java》是Java开发领域的一本经典著作,由著名技术专家Bill Venners编著,被广大Java开发者誉为“四大名著”之一。这本书深入探讨了在企业级Java开发中如何写出高效、可维护和易于理解的代码...

    Effective java 3 学习记录.docx

    Effective Java 3 学习记录 本学习记录主要介绍了 Effective Java 3 中的静态工厂方法和 Builder 模式两部分内容。 一、静态工厂方法 静态工厂方法是指返回类实例的命名规则,例如:from、of、valueOf、instance ...

    effectiveJava课件分享

    在编程领域,特别是Java开发中,"Effective Java"是一本非常经典的书籍,由Joshua Bloch撰写,书中提出了一系列最佳实践和设计原则,以帮助开发者编写出更高效、更安全的代码。根据提供的标题和描述,我们将探讨三个...

    java并发编程实战中文加英文版加源码

    你丫有没有良知,书籍是什么,是希望,是神圣的,你们这些译者简直就是在犯罪 ,不过要是英文功底不好,还是建议买本看吧,谁让你英文水平不如他们呢 《JAVA并发编程实践》随着多核处理器的普及,使用并发成为构建高...

    effective java 读书笔记

    《Effective Java》是Java开发领域的经典著作,作者Joshua Bloch深入浅出地阐述了编写高效、健壮的Java代码的技巧和最佳实践。以下是对该书部分内容的详细解释: 1. **产生和销毁对象** - Item1:静态工厂方法相比...

    《Effective Java》读书分享.pptx

    "Effective Java 读书分享" 《Effective Java》读书分享.pptx 是一本 Java 编程语言指南,旨在帮助开发者编写高质量、可维护的 Java 代码。该书包含 90 个条目,每个条目讨论一条规则,涵盖了 Java 编程语言的...

    Effective Java.zip

    《Effective Java》是一本经典Java编程指南,作者是Joshua Bloch,这本书深入探讨了...以上内容仅是《Effective Java》各章节的部分知识点概述,书中还有更多关于Java编程的最佳实践和深入理解等待读者去发掘和学习。

    Effective.Java_Java8_并发_java_effectivejava_

    目录:一、创建和销毁对象 (1 ~ 7)二、对于所有对象都通用的方法 (8 ~ 12)三、类和接口 (13 ~ 22)四、泛型 (23 ~ 29)五、枚举和注解 (30 ~ 37)六、方法 ...65)九、并发 (66 ~ 73)十、序列化 (74 ~ 78)

    JAVA并发编程实践.pdf

    《JAVA并发编程实践》随着多核处理器的普及,使用并发成为构建高性能应用程序的关键。Java 5以及6在开发并发程序中取得了显著的进步,提高了Java虚拟机的性能以及并发类的可伸缩性,并加入了丰富的新并发构建块。在...

    effective-java 配套代码

    《Effective Java》是Java开发领域的一本经典著作,由Joshua Bloch撰写,书中提出了一系列编程最佳实践和设计模式,帮助开发者写出更高效、更可靠、更易于维护的Java代码。配套代码`effective-java-examples-master`...

    java 并发编程实践 英文版 English

    通过分析常见的并发问题和解决方案,读者可以学习到如何在实际项目中应用并发编程技巧,避免常见的陷阱和错误。 总之,《Java并发编程实践》是一本全面、深入的指南,旨在帮助Java开发者掌握并发编程的核心概念和...

    Effective Enterprise Java(中文) Effective Enterprise Java(中文)

    本书详细介绍企业级计算技术中的常见问题,并描述使用企业级Java平台技术处理这些问题的方法。本书以若干条建议、揸南的形式,言简意赅地介绍了J2EE开发中的微妙之处。无论你是否是Java开发人员,本书都将为你开发...

    effective enterprise java 中文版

    《Effective Enterprise Java》是一本由James Gosling、Bill Venners和Cay S. Horstmann合著的经典著作,旨在帮助Java开发者深入理解和利用企业级Java技术。这本书提供了78条具体的建议,涵盖了从设计模式到并发编程...

Global site tag (gtag.js) - Google Analytics