读完了Venkat Subramaniam的《java虚拟机并发编程》中文译本,对并发编程有了更进一步的认识。
这本书,名字读来让人有些误解,以为是讲java虚拟机的,其实内容讲的是在jvm上的几种编程模型。
这里权当作个笔记,梳理思路,回忆下书的大概内容。
熟悉java的童鞋知道,java是实体与状态绑定的。所以在处理共享状态时,要考虑线程安全问题。
虽然处理状态不可避免,但我们可以有三种方法来处理状态:
1、共享可变性
2、隔离可变性
3、纯粹不可变性
1、共享可变性
我们熟悉的java,通常都是这样处理状态,然后通过锁来锁定共享的变量、方法或者对象。java5之前通常使用内部锁(synchronized),java5之后开始有了更多的并发数据结构和工具。相比内部锁,后者增加了操作的灵活性,降低了锁粒度。
譬如我们使用信号量semaphore
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.Semaphore;
public class BoundedExecutor {
private final Executor exec;
private final Semaphore semaphore;
public BoundedExecutor(Executor exec,Semaphore sem){
this.exec=exec;
this.semaphore=sem;
}
public void submitTask(final Runnable command) throws InterruptedException{
semaphore.acquire();
try {
exec.execute(new Runnable(){
public void run(){
try {
command.run();
} catch (Exception e) {
}finally{
semaphore.release();
}
}
});
} catch (RejectedExecutionException e) {
semaphore.release();
}
}
}
通过semaphore.acquire()来获取进入许可,当当前信号量数量到达上限时,其他请求进入阻塞;线程最后,通过samaphore.release()释放许可。
2、隔离可变性
这里要提到的是基于角色的编程,用角色隔离可变性。oop的缺陷在于对象的状态被封装在对象中,所以只有实例方法才能操作对象的状态。然而不同的线程可以同时调用这些方法,从而导致并发问题的发生。而在基于角色的编程模型中,只允许一个角色操作对象的状态。
角色是一种能够接收消息、处理请求以及发送响应的自由运行的活动,主要被设计用来支持异步化且高效的消息传递机制。
Akka是一个基于scala的角色库,Akka开发人员提供了一套java的API。
抽象类akka.actor.UntypedActor用于表示一个角色,我们实现其方法onReceive()方法来接收消息。
public class HollywoodActor extends UntypedActor{
public void onReceive(final Object role){
System.out.println("Playind "+role+" from Thread "+Thread.currentThread().getName());
}
}
下面是发送消息
public class UseHollywoodActor{
public static void main() throws InterruptedException{
final ActorRef johnnyDepp=Actors.actorOf(HollywoodActor.class).start();
johnnyDepp.sendOneWay("Jack Sparrow");
Thread.sleep(100);
JohnnyDepp.sendOneWay("lrq");
Actors.registry().shutdownAll();
}
}
3、纯粹不变性
既然可变性制造线程安全问题,那何不让一切都不可变。函数式编程语言clojure实现了不可变性,将实体与状态分离。在设计上,值是不可变的,而实体也仅在clojure的事务中是可变的。没错,事务,熟悉数据库的同学一定不陌生。clojure处理并发最著名的莫过于STM(软件事务内存)了。
(def balance (ref 0))
(println "Balance is " @balance)
(dosync
(ref-set balance 100))
(println "Balance is now "@balance)
用ref标记balance为可变(如果 (def balance 0)像这样,这里是值绑定,balance值是不会改变的),用ref-set来改变balance,但前提是在dosync包裹下。如果没有dosync包裹,会直接抛出错误。而java即使忘记加锁或者加错了位置,都不会有任何报警机制。
上述clojure代码,若用java API实现,则
public Class Account{
final private Ref balance;
public Account(final int initialBalance)throws Exception{
balance=new Ref(initialBalance);
}
public int getBalance(){return (Integer)balance.deref();}
public void deposit(final int amount) throws Exception{
LockingTransaction.runInTransation(new Callable<Boolean>(
public Boolean call(){
final int currentBalance = (Integer)balance.deref();
balance.set(currentBalance+amount);
return true;
}
);
);
}
}
Akka中也可以使用事务,可以在java中直接使用Multiverse STM。例如:
return new Atomic<Object>(){
public Object atomically{
//这里的代码会在事务中运行
return resultObject;
}
}.execute();
4、混合使用角色与STM
又是Akka,这里使用的是transactor。
Akka transactor提供了一种将多个角色的执行过程合并到一个事务中的方法。
public class Account extends UntypedTransactor {
private final Ref<Integer> balance = new Ref<Integer>(0);
public void atomically(final Object message) {
if(message instanceof Deposit) {
int amount = ((Deposit)(message)).amount;
if (amount > 0) {
balance.swap(balance.get() + amount);
System.out.println("Received Deposit request " + amount);
}
}
if(message instanceof Withdraw) {
int amount = ((Withdraw)(message)).amount;
System.out.println("Received Withdraw request " + amount);
if (amount > 0 && balance.get() >= amount)
balance.swap(balance.get() - amount);
else {
System.out.println("...insufficient funds...");
throw new RuntimeException("Insufficient fund");
}
}
if(message instanceof FetchBalance) {
getContext().replySafe(new Balance(balance.get()));
}
}
}
并发之路漫漫长矣,clojure要加强学习,java许多东西还要继续看。期待厚积薄发。
有道是,长风破浪会有时,直挂云帆济沧海。