《Effective java》读书笔记9——线程并发

本文介绍了Java中的synchronized关键字及其作用,解释了线程同步的重要性,讨论了线程活性失败和安全性失败的问题,并通过实例展示了volatile关键字在轻量级线程同步中的应用。此外,文章还探讨了并发修改异常、死锁问题及其解决方案,并提到了Java并发集合和并发线程框架如线程池、定时任务的使用。
摘要由CSDN通过智能技术生成

synchronized同步关键字:

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

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

Java语言规范保证读或者写一个变量是原子性操作(虽然long和double的读写操作没有被规定为原子性操作,但是JVM的各个厂商都把他们实现为原子性操作),因此读取一个变量可以保证返回的值是某个线程保存在该变量中的,即使多个线程在没有同步的情况下并发地修改该变量也是如此。

但是对于共享的可变数据访问如果不同步,则会引起意想不到的结果,例子如下:

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

上述程序本来期待允许大约1秒左右,之后主线程将stopRequsted设置为true,致使后台线程的循环终止,但是这个程序在真实情况下是永远不会终止。

线程活性失败:由于对共享的可变数据访问不同步,则不能保证线程何时看到主线程对共享变量的修改。上述例子就是由于线程活性失败导致的,没有同步,java虚拟机将循环代码:

while(!done){
	i++;
}

优化转变成了如下:

if(!done){
	while(true){
	i++;
}
}

由于java虚拟机的提升优化,导致了线程活性失败,修正这个问题的一种方式是同步访问stopRequested域,代码如下:

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

volatile关键字:

synchronized同步方法或者同步代码块可以保证方法互斥访问和同步通信,但是性能开销相对比较大,volatile关键字是一个轻量级的线程同步,虽然volatile不执行互斥访问,但可以保证任何一个线程在读取变量域值的时候都将看到最近刚刚被写入的值,StopThread例子使用volatile关键字修改如下:

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

注意:使用volatile时要确保变量必须是原子性操作,否则产生安全性失败,一个产生不同自增序列号的例子如下:

private static volatile int nextSerialNumber = 0;
public static int generateSerialNumber(){
	return nextSerialNumber++;
}

由于自增运算符++不是原子性操作,它会在nextSerialNumber域中执行两项操作:首先它读取值,然后写回一个新植,相当于原来的值再加上1。

线程安全性失败:如果第二个线程在第一个线程读取旧值和写回新植期间读取这个域,则第二个线程就会与第一个线程一起看到同一个值,并返回相同的序列号。

修正这个安全性失败的方法有两个:

(1).同步generateSerialNumber()方法,删除volatile关键字。

(2).使用原子数据类型:

private static final AtomicLong nextSerialNum = new AtomicLong();
public static long generateSerialNumber(){
	return nextSerialNum.getAndIncrement();
}

原子性数据类型的getAndIncrement()方法是一个可以实现自增的原子操作。

避免在同步中调用外来方法:

为了避免线程的活性失败和安全性失败,在一个被同步方法或者代码块中,永远不要放弃对客户端的控制,即在一个被同步的区域内部,不要调用设计成要被覆盖的方法,或者由客户端以函数对象的形式提供的方法,否则由于同步块无法控制这些外来的方法,会导致异常、死锁或者数据损坏。

使用观察者模式实现一个向集合容器添加元素时发出通知的例子如下:

//集合观察者
public interface SetObserver<E>{
	public void added(ObserableSet<E> set, E element);
}
//被观察的集合主题
public class ObservableSet<E> extends HashSet<E>{
	//观察者集合
	private final List<SetObserver<E>> observers = new ArrayList<SetObserver<E>>();
	//注册观察者
public void addObserver(SetObserver<E> observer){
	synchronized(observers){
	observers.add(observer);
}
}
//取消注册观察者
public void removeObserver(SetObserver<E> observer){
	synchronized(observers){
	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;
}
}

打印0-99的数字,基本调用如下:

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);
}
}

程序正常工作。

如果要求当观察到的值是23时,观察者取消订阅,则将上述例子中添加观察者代码修改如下:

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。

出现并发修改异常的原因在于,当被观察集合ObservableSet 的notifyElementAdded方法调用观察者外来的added方法时,观察者正处于遍历observers列表中,当观察者的added方法调用被观察集合的removeObserver方法,从而调用observers.remove方法来删除观察者,当在遍历列表过程中,将一个元素从列表中删除是非法的。

被观察集合通知观察者notifyElementAdded方法中的遍历是在一个同步块中,可以防止并发修改,但是却无法防止迭代线程本身回调被观察集合中取消观察者的removeObserve方法,也无法防止修改它的观察者列表(observers.remove)。

有人可能想到使用另一个线程来取消预订的观察者来解决并发修改异常,例子如下:

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,但是无法获得锁,因为被观察集合的通知观察者方法中主线程已经拥有了观察者列表observes锁,主线程一直等待取消观察者线程删除观察者,而取消观察者的现在却一直等待获取观察者列表锁,造成死锁。

实现当数值等于23时取消观察者,并且没有并发修改异常和死锁的解决方法如下:

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);
}
}

被观察集合在通知观察者方法中不直接对观察者列表进行操作,而是对观察者集合的快照拷贝进行操作,既避免了并发修改异常,同时又避免了死锁问题。

并发集合:

JDK1.5之后引入的并发集合可以更加优雅地解决上述问题,例子代码如下:

private final List<SetObserver<E>> observers = new CopyOnWriteArrayList<SetObserver<E>>();
public void addObservers(SetObserver<E> observer){
	observers.add(observer);
}
public boolean removeObservers(SetObserver<E> observer){
	observers.remove(observer);
}
public void notifyElementAdded(E element){
	for(SetObserver<E> observer : observers){
	observer.added(this, element);
}
}

并发集合中的CopyOnWriteArrayList,CopyOnWriteArraySet等在多线程操作时会将底层的数组拷贝一份以实现针对快照拷贝写操作的功能,由于原始集合的数组永远不改变,因此在迭代过程中不需要锁定,速度非常快。

并发线程框架:

JDK1.5之后引入了并发线程框架可以更加优雅地实现异步多线程问题,同时还提供了线程池,定时重复任务等强大的功能,并发线程框架的基本使用如下:

//创建一个单线程线程执行器
ExecutorService executor = Executors.newSingleThreadExecutor();
//执行线程任务
executor.execute(runnable);
//任务结束线程终止
executor.shutdown();

线程框架创建线程池的方法如下:

//创建缓冲线程池
ExecutorService executor = Executors.newCachedThreadPool();
//创建固定数目线程池
ExecutorService executor = Executors.newFixedThreadPool(int threadNum);

线程框架创建定时任务方法如下:

//创建单线程定时任务
ScheduleExecutorService executor = Executors.newSingleThreadScheduleExecutor();
//创建线程池定时任务
ScheduleExecutorService executor = Executors.newScheduleThreadPool(int threadNum);
//执行给定延迟时间后的一次操作
executor.schedule(Runnable runnable, long delay, TimeUnit unit);
//执行固定频率任务,给定首次延迟,然后间隔给定时间重复执行
executor.scheduleAtFixedRate(Runnable runnable, long initialDelay, long period, TimeUnit unit);
//执行固定延迟任务,给定首次延迟,然后每隔给定延迟时间重复执行
executor.scheduleWithFixedDelay(Runnable runnable, long initialDelay, long delay, TimeUnit unit);

Timer和线程池定时任务的对比:

(1).Timer实现定时任务更加简单,Timer只用一个线程来执行任务,在面对长期运行的任务时,会影响到定时的准确性。如果Timer唯一的线程抛出未被捕获的异常,Timer就会停止执行.

(2).线程池定时任务支持多个线程更加灵活,可以优雅地从抛出的未受检异常的任务中恢复。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值