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

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

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

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

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

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

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

[b]遍历的三种同步处理[/b]
(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() {
}
}
}


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

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

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

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

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

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

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

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

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

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

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

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

[b]乐观更新[/b](非阻塞)
乐观更新的一般原理:
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);
}
}


[b]关于线程的设计模式[/b]

[b]每消息一线程[/b]
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();
}});
}
}


[b]工作者线程[/b]
与前面的不同之处在于, 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);
}
}


[b]完成回调[/b]
回调实际上是一种观察者模式的实现.
描述:当客户端向服务器端发送一个单向消息的方式激活一个任务, 服务器端在完成任务之后, 向客户端发送一个单项回调消息通知任务已经完成.
适用场景:当需要读入一个特定的文件, 但是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
}
}


[b]协作线程[/b]
这个这个模式就是讲的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
}
}


[b]Future[/b]
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;
}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值