java代码优化(九)——并发

同步访问共享的可变数据

①关键字synchronized可以保证同一时刻只有一个线程执行一个方法或者一段代码块。

②同步的意义:同步不仅可以阻止一个线程看到对象处于不一致的状态之中,它还可以保证进入每个同步代码块或者同步方法的线程都可以看到由同一个锁保护的之前的修改效果。

③java语言规范保证读写一个变量是原子的,换句话说,读取一个非long或者非double类型的变量时,可以保证返回的值是线程保存在该变量中的。

④对于“在读取原子变量时应该避免使用同步”这种说法是错误的,因为修改原子变量并不一定保证某个线程修改的值对其他线程是可见的,无论如何保证同步都是必要的,java语言规范规定任何一个线程对对象所做的变化对其他线程来说都是可见的。

针对以上第四点,我将举例说明不使用同步,它的后果将非常严重,请看下面的代码:

public class UnitTest {

	private static boolean flag;

	public static void main(String[] args) throws InterruptedException {
		new Thread(new Runnable() {
			public void run() {
				int i = 0;
				while(!flag){
					i++;
				}
			}
		}).start();
		TimeUnit.SECONDS.sleep(1);
		flag = true;	
	}
}

这段代码的意图是在第一个线程执行1s后,main线程将flag置为true,那么第一个线程将会停止,我们期望看到的结果是这个程序将在1秒后停止执行,但实际上它将会一直执行,原因是某个线程修改原子变量并不一定保证对其他线程可见,这里main线程对flag进行修改,然而第一个线程并不知道flag被修改了,这就是没有同步导致的。

那么解决这个问题方法就是同步读和取flag的方法:

public class UnitTest {

	private static boolean flag;

	public static void main(String[] args) throws InterruptedException {
		new Thread(new Runnable() {
			public void run() {
				int i = 0;
				while(!getFlag()){
					i++;
				}
			}
		}).start();
		TimeUnit.SECONDS.sleep(1);
		setFlag();
	}
	
	public static synchronized void setFlag(){
		flag = true;
	}
	
	public static synchronized boolean getFlag(){
		return flag;
	}

}

读取原子变量的方法被同步了,那么这个原子变量就是同步的,某个线程对它进行的读取操作对其他线层都是可见的了,那么还有没有其他方法来保证flag线程同步?答案是肯定的!可以使用关键字volatile,请看下面的解决方法:

public class UnitTest {

	//volatile可以保证这个变量的每次变动对每个线程都是可见的
	private static volatile boolean flag;

	public static void main(String[] args) throws InterruptedException {
		new Thread(new Runnable() {
			public void run() {
				int i = 0;
				while(!flag){
					i++;
				}
			}
		}).start();
		TimeUnit.SECONDS.sleep(1);
		flag = true;
	}

}

volatile可以保证这个变量的每次变动对每个线程都是可见的,这样就可以省略读取变量的synchronized修饰符。

使用volatile需要格外的小心,请看下面的例子:

	private static volatile int serialNumber = 0;
	
	public static int getNextSerialNumber(){
		return serialNumber++;
	}

这个方法的目的是获得不同的值,serialNumber已经被同步了,这个方法看似没有问题,但实际上并不是!++并不是原子的,++是先将变量读出来,再将值写进去,当多个线程调用这个方法时,由于getNextSerialNumber是没有同步的,所以可能在一个线层取旧值写新值的过程,其他线程将会读到没有更新过来的旧值。解决这个问题的方法是在getNextSerialNumber方法上加synchronized修饰符,这样可以保证读取serialNumber不存在交叉读取的问题,而且可以将volatile去掉。

另外还需要介绍一种方法:使用AtomicLong,这个类是java.util.concurrent包下的,所以它本身就支持同步调用,请看下面的示例:

	private static AtomicLong serialNumber;
	
	public static long getNextSerialNumber(){
		return serialNumber.getAndIncrement();
	}

getAndIncrement这个方法将会原子地在当前值上加1,内部实现是线程安全的。

总而言之,共享可变数据就需要对读取可变数据的方法都进行同步方法,否则就无法保证数据能被正确的传递给其他线程或则修改正确的值。针对以上的问题最好的解决方法是不去共享可变的数据,也就是共享不可变的数据。

避免过度同步

过度的使用同步有可能会导致性能降低,死锁,抛出异常以及其他问题。请不要在同步方法中调用外部方法!!!请看下面的例子,它将抛出java.util.ConcurrentModificationException异常,这是因为在notifyElementAdded方法实现了同步,在这个过程试图将一个元素从这个list集合中删除这就导致了后台抛出异常。

public interface SetObserver<E> {
	void added(ObservableSet<E> set,E e);
}
public class ForwardingSet<E> implements Set<E>{

	private Set<E> s;
	
	public ForwardingSet(Set<E> s){
		this.s = s;
	}
	
	@Override
	public int size() {
		return s.size();
	}

	@Override
	public boolean isEmpty() {
		return s.isEmpty();
	}

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

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

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

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

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

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

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

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

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

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

	@Override
	public void clear() {
		s.clear();
	}

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

	private final List<SetObserver<E>> observers = new ArrayList<SetObserver<E>>();
	
	public ObservableSet(Set<E> s) {
		super(s);
	}
	
	public void add(SetObserver<E> observer){
		synchronized (observers) {
			observers.add(observer);
		}
	}
	
	public void remove(SetObserver<E> observer){
		synchronized (observers) {
			observers.remove(observer);
		}
	}
	
	public void notifyElementAdded(E element){
		synchronized (observers) {
			for (SetObserver observer : observers) {
				observer.added(this, element);
			}
		}
	}

	@Override
	public boolean add(E element) {
		boolean add = super.add(element);
		if(add){
			notifyElementAdded(element);
		}
		return add;
	}
	
	@Override
	public boolean addAll(Collection<? extends E> c) {
		boolean result = false;
		for (E e : c) {
			result |= add(e);
		}
		return result;
	}

}

测试类:

	
	public static void main(String[] args) {
		ObservableSet<Integer> set = new ObservableSet<Integer>(new HashSet<Integer>());
		set.add(new SetObserver<Integer>() {
			@Override
			public void added(final ObservableSet<Integer> s, Integer e) {
				System.out.println(e);
				if(e==23){
					s.remove(this);
				}
			}
		});
		for (Integer i = 0; i < 100; i++) {
			set.add(i);
		}
	}
	

请看下面的测试类,它将造成死锁,由于主线程拥有observers对象锁,而s.remove(observer)试图获得该锁,但是无法获得该锁,导致主线程一直在等待后台线程完成对observer的删除,从而造成死锁:

	public static void main(String[] args) {
		ObservableSet<Integer> set = new ObservableSet<Integer>(new HashSet<Integer>());
		set.add(new SetObserver<Integer>() {
			@Override
			public void added(final ObservableSet<Integer> s, Integer e) {
				System.out.println(e);
				if(e==23){
					ExecutorService service = Executors.newSingleThreadExecutor();
					final SetObserver<Integer> observer = this;
					service.execute(new Runnable() {
						@Override
						public void run() {
							s.remove(observer);
						}
					});
				}
			}
		});
		for (Integer i = 0; i < 100; i++) {
			set.add(i);
		}
	}
	

总而言之,为了避免线程死锁,性能降低,数据破坏或者抛出异常,请不要是在同步区域内部调用外来方法。

executors和task优先于线程

自jdk 1.5开始,java在java.util.concurrent包下增加了Executors Iframework,它是一个非常灵活的基于接口的执行任务工具。它可以很方便的创建一个工作队列:

		ExecutorService executor = Executors.newSingleThreadExecutor();//创建一个只有线程的线程池
		//在execute方法中提供具体的task
		executor.execute(new Runnable() {
			@Override
			public void run() {
				System.out.println(Thread.currentThread().getName());
			}
		});
		executor.shutdown();//关闭线程池

Executors有4种创建线程池的方法:

newCachedThreadPool():适用于轻负载的小程序。

newFixedThreadPool():适用于高负载,高并发的程序。

newSingleThreadExecutor():只实例化1个线程的线程池。

newScheduledThreadPool():可以指定线程周期,调度任务。

并发工具优先于wait和notify

请在新代码中使用java.util.concurrent包下的高级并发工具,如果在维护遗留的代码中用到了wait和notify,请确保在循环中使用wait,而且notifyAll优先于notify

线程安全性的文档化

无条件的线程安全类请使用私有对象锁来代替synchronized修饰符。

有条件的线程安全类请在文档中指明:调用哪个方法需要需要外部同步,以及在执行这些序列的时候需要或者哪把锁。

慎用延迟初始化

一般而言,普通的初始化优于延迟初始化,但是在考虑性能问题是可以使用双重校验锁机制、单重校验锁机制或者lazy initialization holder class模式。

①对于只需要实例化一次的实例域而言,请选择双重校验锁机制。

②对于可以重复实例化的实例域而言,请使用单重校验锁机制。

③对于静态域,请使用lazy initialization holder class模式。

lazy initialization holder class模式:

public class StaticField {
	static final String field = computeFiledValue();
	public static String getField() {
		return StaticField.field;
	}
	private static String computeFiledValue() {
		return "hello world";
	}	
}

不要依赖于线程调度器

不要让应用程序的正确性依赖于线程调度器,否则结果可能即不健壮,又无法移植。也不要依赖于Thread.yield和线程优先级。

避免使用线程组

线程组的安全性很差,所以就不要使用它。

猜你喜欢

转载自blog.csdn.net/ZixiangLi/article/details/86534827