`

《java并发编程设计原则与模式》浅读笔记

    博客分类:
  • Book
阅读更多

本书基本上是围绕线程和同步, 锁来讲如何实现并发编程, 并结合一些设计模式从中找到一些并发编程的规律, 加以总结即成此书. 而且由于作者也是concurrent包的贡献者, 因此里面也基本上是结合concurrent中的一些实现来做例子.
看完了这本书, 总的感觉是第二章和第四章的内容不错(有些内容需要反复阅读才能理解), 第二章说的是独占, 主要针对synchronized的用法, 第四章是创建线程, 实际上是线程的一些高级用法, 学到不少东东, 第三章看得让人崩溃,不知所云:(.
还有作者总结的原则不是很明显, 没有像23种设计模式那样严谨和中规中矩.导致的后果是看晦涩难懂的文字还不如直接看程序更容易理解作者讲的是什么意思.

synchronized和锁
Object类和他的子类的每一个实例在进入一个同步(synchronized)方法前加锁, 并在离开这个方法时自动释放这把锁. 同步块需要一个参数来指明对哪一个对象加锁. 最常用的参数是this, 它意味着锁住当前正在执行的方法所属的对象. 当一个线程持有一把锁之后, 其他线程必须阻塞, 等待这个线程释放这把锁. 加锁对于非同步的方法不起作用. 因此即便另一个线程持有这个锁, 这个非同步方法也可以执行.

同步的一些简单的原则
永远只是在更新对象的成员变量时加锁.
永远只是在访问有可能被更新的对象的成员变量时才加锁.
永远不要在调用其他对象的方法时加锁.

不变性
如果一个对象的状态不能被改变, 那么它永远也不会遇到由于多个操作以不同的方式改变其状态而导致的冲突和不一致的现象.
具有不变性的最简单的做法就是对象中根本没有数据, 因此他们的方法都是没有状态的, 也就是说这些方法不依赖于任何对象的任何数据.

在构造函数结束之前, 最好禁止访问对象的数据.

对象和锁
包含基本类型的数组对象也是拥有锁的对象, 但是他的每一个基本元素却没有锁(不能把数组元素声明为volatile).
synchronized关键字不属于方法签名的一部分, 所以当子类覆盖父类方法时, synchronized修饰符不会被继承. 因此接口中的方法不能声明为synchronized.同样地, 构造函数也不能声明为synchronized(尽管构造函数中的程序块可以声明为synchronized).
子类和父类的方法实用同一个锁, 但是内部类的锁和他的外部类无关. 然而一个非静态的内部类可以锁住它的外部类. 就像下面的样子:
synchronized(OuterClass.this){...}
锁操作基于"每线程"而不是基于"每调用"
synchronized和atomic不是等价的, 但是同步可以实现原子操作.

遍历的三种同步处理
(1)就是将整个遍历操作方法加上synchronized
(2)在遍历循环内部加synchronized, 比如这样写:
for(int i = 0; true; i++){
	Object obj = null;
	synchronized(v){
		if (i < v.size()){
			obj = v.get(i);
		}else{
			break;
		}
	}
}

另外一个极端的做法:
		synchronized(v) {
			snapshot = new Object[v.length];
			for (int i = 0; i < snapshot.length; i++) {
				snapshot[i] = v[i];
			}
		}
		
		for (Object object : snapshot) {
			...
		}

(3)失败即放弃, 采用版本化的迭代变量
public class ExpandableArray {
	private int version = 0;
	private int cap = 0;
	private Object[] data;
	private int size = 0;

	public int size() {
		return size;
	}

	public synchronized Object get(int i) {
		return data[i];
	}

	public synchronized void add(Object x) {
		// 
		data[size++] = x;
		version++;
	}

	public ExpandableArray(int cap) {
		this.cap = cap;
	}

	public synchronized void removeLast() {
		data[--size] = null;
		++version;
	}

	public synchronized Iterator iterator() {
		return new EAIterator();
	}

	protected class EAIterator implements Iterator {
		private final int currentVersion;
		private int currentIndex = 0;

		public EAIterator() {
			currentVersion = version;
		}

		public boolean hasNext() {
			return currentIndex < size;
		}

		public Object next() {
			synchronized(ExpandableArray.this) {
				if (currentVersion != version) {
					throw new RuntimeException("concurrent modified error");
				}else if (currentIndex == size) {
					throw new RuntimeException("no such element");
				}else {
					return data[currentIndex++];
				}
			}
		}

		public void remove() {
		}
	}
}


静态和单例
完全初始化静态变量并不会增加很多系统的启动开销, 除非初始化既消耗资源又很少需要, 否则简单的方式就是把单例数据声明为static final类型

如果每个线程创建一个实例比每个程序创建一个实例更恰当, 这时用ThreadLocal比用单例更恰当.

死锁
死锁是在两个或多个线程都有权限访问两个或多个对象, 并且每个线程都已经得到一个锁的情况下等待其他线程已经得到的锁

java内存模型
为了更好的理解java内存模型, 我们假设每个线程都运行在不同的CPU上, 这样可以更好理解java内存模型的合理性:持有线程的每个CPU都有自己独立的寄存缓存器, 同时所有的CPU都需要跟共享的主存储器通讯来交换数据.

volatile
如果每一个成员变量都声明为volatile, 那么在写线程做下一步存储操作之前, 吸入这个volatile成员变量的数据在主存储器中刷新, 并使其对其他线程可见, 也就是说, 为了这个目的, 成员变量会立即被刷新. 读线程在每次使用volatile成员变量之前都要重新读入数据.

对没有完全没有建好的对象进行引用很不好, 在构造函数内部启动一个线程也是很危险, 尤其是当一个类可能被子类化的时候.

当一个线程结束的时候, 所有写入数据都将被刷新到主存储器中.

把一个数据声明为volatile和同步的区别只是没有使用锁. 尤其对于复合读写操作, 例如对volatile变量进行++操作在执行时并不具有原子性.

因为没有锁的参与, 所以使用volatile比同步要划算一些, 或者至少开销是相同的. 然而如果在方法内部频繁使用volatile成员变量, 则整个性能就会下降. 这时整个方法声明为同步会更好些.

如果仅仅需要保证在多个线程之间正确访问成员变量

ThreadLocal
ThreadLocal类内部维护了一张相关数据(Object引用)和Thread实例列表, 其中的set和get方法可以存取当前Thread空值的数据. 从ThreadLocal类继承来的InheriableThreadLocal类可以自动的把本线程的变量传递给创建的任何一个线程.
很多实用ThreadLocal的设计都被视为单例的扩展. 多数ThreadLocal应用程序为每个线程创建了一个资源的事例, 而不是为每个程序创建一个, ThreadLocal变量通常被声明为静态的, 并且是包范围内可见, 所以这些变量可以在运行于某个线程的一组方法中访问.

令牌
在某些语境或上下文中, 包含独占资源的协议被称作令牌, 一些并行和分布式算法依赖于下面的思想:一个时刻只有一个对象拥有令牌

乐观更新(非阻塞)
乐观更新的一般原理:
1.得到当前状态表示的一个copy(在持有锁的时候)
2.创建一个新的状态表示(不持有任何锁)
3.只有在老状态在被获取后且没有被更新的情况下, 才能被转换成新的状态.
这个也是并发包中的atomic class的compareAndSet()方法的实现原理
public class OptimisticDot {
	static class ImmutablePoint {
		int x, y;

		ImmutablePoint(int x, int y) {
			this.x = x;
			this.y = y;
		}
	}
	
	ImmutablePoint loc;
	
	public OptimisticDot(int x, int y) {
		loc = new ImmutablePoint(x, y);
	}
	
	public synchronized ImmutablePoint location() {
		return loc;
	}
	
	public synchronized boolean commit(ImmutablePoint assumed, ImmutablePoint next) {
		if (loc == assumed) {
			loc = next;
			return true;
		}else {
			return false;
		}
	}
	
	public synchronized void moveTo(int x, int y) {
		loc = new ImmutablePoint(x, y);
	}
	
	public void shiftX(int data) {
		boolean success = false;
		do {
			ImmutablePoint old = location();
			ImmutablePoint next = new ImmutablePoint(old.x + data, old.y);
			success = commit(old, next);
		}while(success);
	}
}


关于线程的设计模式

每消息一线程
public class Host {
	interface Handler{
		void handle();
	}
	Handler handler = new Handler() {
		@Override
		public void handle() {
			// do something
		}
		
	};
	private void updateState() {
		//update state...
	}
	public void req() {
		updateState();
		new Thread() {
			@Override
			public void run() {
				handler.handle();
			}
		}.start();
	}
}

多个并发任务能比同样多的任务串行执行运行的更快, 这种策略能提高系统的吞吐量, 通常这些任务是与IO资源或者计算资源有关, 并且运行在一个多处理器的系统上, 而且客户端也无序等待彼此的任务完成.
缺点是由于创建线程远比直接方法调用开销要大, 所以每消息一线程会增加请求的反应时间.
为了增加扩展性, 将Thread加以封装, 引入一个Executor:
public class HostWithExecutor extends Host{
	interface Executor{
		void execute(Runnable r);
	}
	
	class DefaultExecutor implements Executor{

		@Override
		public void execute(Runnable r) {
			new Thread(r).start();
		}
	}
	private Executor executor;
	
	public HostWithExecutor(Executor executor) {
		super();
		this.executor = executor;
	}
	
	public void req() {
		updateState();
		executor.execute(new Runnable() {
			@Override
			public void run() {
				handler.handle();
			}});
	}
}


工作者线程
与前面的不同之处在于, Executor的实现中包含了一个队列来执行不断接收到的请求, 这个队列池就被称之为工作者线程.
public class PlainWorkerPool implements Executor{
	protected final Queue<Runnable> queue;
	
	public PlainWorkerPool(Queue<Runnable> queue, int workerCount) {
		this.queue = queue;
		for(int i = 0; i < workerCount; i++) {
			activate();
		}
	}

	private void activate() {
		new Thread() {
			@Override
			public void run() {
				for(;;) {
					Runnable r = queue.poll();
					r.run();
				}
			}
		}.start();
	}

	@Override
	public void execute(Runnable r) {
		queue.add(r);
	}
}


完成回调
回调实际上是一种观察者模式的实现.
描述:当客户端向服务器端发送一个单向消息的方式激活一个任务, 服务器端在完成任务之后, 向客户端发送一个单项回调消息通知任务已经完成.
适用场景:当需要读入一个特定的文件, 但是IO操作非常的缓慢, 在这个过程中, 你又不想让程序"死"在那里(程序无法响应其他操作), 一种解决办法就是创建一个文件读取服务(FileReader), 当服务完成之后, 向应用程序发送一条消息, 而此后应用程序就可以执行相应的功能了.
public interface FileReader {
	void read(String fileName, FileReaderClient client);
}
public interface FileReaderClient {
	void readCompleted(String fileName, byte[] data);
	void readFailed(String fileName, IOException ex);
}
public class FileReaderImpl implements FileReader {

	@Override
	public void read(String fileName, FileReaderClient client) {
		byte[] buffer = new byte[1024];
		try {
			FileInputStream fis = new FileInputStream(fileName);
			fis.read(buffer);
			client.readCompleted(fileName, buffer);
		}catch (IOException e) {
			client.readFailed(fileName, e);
		}
	}
}
public class FileReaderClientImpl implements FileReaderClient {
	FileReader reader;
	
	public void read(String fileName) {
		reader.read(fileName, this);
	}

	@Override
	public void readCompleted(String fileName, byte[] data) {
		//TODO
	}

	@Override
	public void readFailed(String fileName, IOException ex) {
		//TODO
	}
}


协作线程
这个这个模式就是讲的Thread.join的用法, 将一个耗时的操作封装在一个线程中执行, 然后接着继续主线程的执行, 当需要耗时线程的执行结果时join一下, 从耗时线程中获得最终所需要的结果, 进行主线程后面的内容.
作者用一个获取图片数据, 并加以显示的例子来说明协作线程模式:
public interface Pic {
	byte[] getImage();
}
public interface Randerer {
	Pic rander(URL src);
}
public class PictureApp {
	final Randerer renderer = new StandardRenderer();
	public void show(final URL source) {
		class Waiter implements Runnable{
			private Pic result = null;
			public Pic getResult() {
				return result;
			}
			@Override
			public void run() {
				result = renderer.rander(source);
			}
		}
		
		Waiter waiter = new Waiter();
		Thread t = new Thread(waiter);
		t.start();
		
		// other do something
		
		try {
			t.join();
		} catch (Exception e) {
			return;
		}
		
		Pic pic = waiter.getResult();
		// show the pic
	}
}


Future
future是Thread.join的另一种替代物, 它将等待过程封装到了获取图片结果里面. 就是这段代码:
		Waiter waiter = new Waiter();
		Thread t = new Thread(waiter);
		t.start();
		
		// other do something
		
		try {
			t.join();
		} catch (Exception e) {
			return;
		}

对于应用程序来说, 它不再需要知道是否需要经过等待才能拿到结果. 更不需要知道取图片的内容处理原理, 这样对使用者来说更简单.
public class PictureAppWithFuture {
	private final Renderer renderer = new AsyncRenderer();
	
	public void show(final URL src) {
		Pic pic = renderer.render(src);
		//do something
		
		// 既不需要专门的另起一个线程, 也不需要通过join等子线程执行完
		byte[] image = pic.getImage();
		if (image != null) {
			System.out.println("show image ");
		}
	}
	
	public static void main(String[] args) throws Exception {
		PictureAppWithFuture app = new PictureAppWithFuture();
		app.show(new URL("url string"));
	}
}
public class AsyncRenderer implements Renderer {
	private final Renderer renderer = new StandardRenderer();
	
	static class FuturePic implements Pic{
		private Pic pic = null;
		private boolean ready = false;
		synchronized void setPic(Pic pic) {
			this.pic = pic;
			ready = true;
			notifyAll();
		}
		@Override
		public byte[] getImage() {
			// 原来的等待操作被封装在这里
			while(!ready) {
				try {
					wait();
				} catch (Exception e) {
					//return null;
				}
			}
			return pic.getImage();
		}		
	}

	@Override
	public Pic render(final URL src) {
		final FuturePic pic = new FuturePic();
		// 另起一个线程去拿图片内容
		new Thread(new Runnable() {
			@Override
			public void run() {
				// 耗时的操作
				pic.setPic(renderer.render(src));
			}}).start();
		return pic;
	}
}

分享到:
评论
2 楼 mengsina 2013-03-18  
future是Thread.join的另一种替代物
有道理
1 楼 pan_java 2009-06-04  
对多线程的了解又加深了一步.之前看过多线程的设计模式也是受益非浅.不过是基于java1.4,没有学习到新特性的一些应用!

相关推荐

    java并发编程实践pdf笔记

    这本书的读书笔记涵盖了多个关键知识点,旨在帮助读者深入理解Java并发编程的核心概念。 1. **线程和进程的区别** - **线程** 是程序执行的最小单位,一个进程中可以有多个线程同时执行,共享同一块内存空间,通信...

    Java并发编程学习笔记.rar

    通过学习上述知识点,并结合"Java并发编程学习笔记"中的内容,开发者可以更深入地理解Java并发编程,从而设计出更加高效、稳定的并发程序。无论是对初学者还是经验丰富的开发者来说,这本书都是一份宝贵的参考资料。

    Java并发编程与高并发解决方案-学习笔记-www.itmuch.com.pdf

    本文将基于文档《Java并发编程与高并发解决方案-学习笔记***.pdf》中提供的内容,来详细阐述并发编程和高并发的基本概念、CPU多级缓存与缓存一致性、以及Java内存模型。 ### 并发与高并发概念 在现代多线程编程中...

    Java并发编程笔记

    ### Java并发编程知识点详解 #### 一、线程状态与管理 在Java中,线程具有多种状态,这些状态的变化反映了线程在其生命周期中的不同阶段。理解这些状态及其转换对于编写高效、健壮的并发程序至关重要。 - **NEW**...

    读书笔记-Java并发编程实战-基础篇

    在Java并发编程中,数据的封装与访问控制、线程安全性的考量、同步机制的使用是重要的基础概念和技巧。以下是从给出的文件内容中提取出的详细知识点: 1. 数据封装与访问控制:确保内部私有数据不被轻易访问,并且...

    java并发编程实践笔记

    ### Java并发编程实践笔记知识点详解 #### 一、保证线程安全的方法 1. **不要跨线程访问共享变量:** 当多个线程共享某个变量时,若其中一个线程修改了该变量,其他线程若没有正确同步,则可能读取到错误的数据。...

    java并发编程实践笔记资料.pdf

    Java并发编程实践笔记是一份关于Java并发编程的实践笔记,涵盖了多种关于线程安全、并发编程的实践经验和原则。下面是从笔记中总结的知识点: 1. 保证线程安全的三种方法:不要跨线程访问共享变量,使用final类型的...

    《java并发编程实战》读书笔记-第4章-对象的组合

    《java并发编程实战》读书笔记-第3章-对象的共享,脑图形式,使用xmind8制作 包括线程安全类设计、实例封闭、线程安全性委托、现有线程安全类中添加功能和文档化同步策略等内容

    Java并发编程与高并发解决方案笔记-基础篇.docx

    Java并发编程与高并发解决方案是开发高性能应用的关键技术。在基础篇中,主要涉及以下几个重要知识点: 1. **并发编程基础** - **并发**:并发是指在一个时间段内,多个线程交替执行,使得系统看起来像是同时处理...

    读书笔记:java学习笔记(包括设计模式并发编程网络编程等内容.zip

    读书笔记:java学习笔记(包括设计模式并发编程网络编程等内容

    Java并发编程学习笔记.

    Java并发编程是Java开发中的重要领域,它涉及到多线程、同步、锁机制、线程池等关键概念,是提高程序性能和效率的关键技术。在Java中,并发编程的运用可以充分利用多核处理器的能力,实现高效的多任务处理。以下是对...

    [原]Java并发编程实践-读书笔记

    《Java并发编程实践》这本书是Java开发者深入理解并发编程的重要参考。以下是对书中关键知识点的总结: 1. **线程和进程的区别** - **线程**:是程序执行的最小单位,一个进程中可以有多个线程,它们共享同一块...

    读书笔记:Java高并发编程详解多线程与架构设计.zip

    读书笔记:Java高并发编程详解多线程与架构设计

    Java并发编程与高并发解决方案-学习笔记

    ### Java并发编程与高并发解决方案知识点总结 #### 一、并发与高并发基本概念 ##### 1.1 并发 - **定义**: 指一个程序在同一时刻拥有两个或更多的线程,这些线程可以在单核或多核处理器上运行。 - **单核处理器上...

    java编程思想读书笔记

    ### Java编程思想读书笔记 #### 一、Java与C++的区别及内存管理 在学习Java的过程中,我们常常会拿它与C++进行比较。这两门语言虽然有着相似之处,但也有许多不同点。 1. **内存管理:** - C++提供了更为底层的...

    读书笔记:参考Java高并发编程详解多线程与架构设计汪文君学习笔记及源码.zip

    读书笔记:参考Java高并发编程详解多线程与架构设计汪文君学习笔记及源码

    读书笔记:算法 并发 函数式编程 java语言拾遗 手写设计模式.zip

    读书笔记:算法 并发 函数式编程 java语言拾遗 手写设计模式

    java并发编程与高并发解决方案笔记

    Java并发编程是开发人员在构建高并发应用时必须掌握的关键技术。高并发环境下,接口幂等性成为确保系统稳定性和正确性的必要条件。幂等性指的是一个操作无论执行多少次,其结果始终相同,这对于避免重复操作导致的...

    Java并发编程与高并发解决方案-学习笔记.pdf

    学习Java并发编程时,锁的概念是无法回避的。锁是同步的一种机制,用来控制多个线程访问共享资源的顺序。在Java中,锁主要有两种类型:内置锁和显示锁。内置锁使用synchronized关键字实现,而显示锁则是通过java....

Global site tag (gtag.js) - Google Analytics