Java并发(十二)并发模型

极客时间:Java并发编程实战

1 Actor模型:面向对象原生的并发模型

  • 概念

Actor模型本质上是一种计算模型,基本的计算单元称为Actor,Actor之间通过消息进行通信;

在Actor模型中,所有的计算都是在Actor中执行的。

在面向对象编程里面,一切都是对象;
在Actor模型里,一切都是Actor,并且Actor之间是完全隔离的,不会共享任何变量

在Java领域,除了可以使用Akka来支持Actor模型外,还可以使用Vert.x,不过相对来说Vert.x更像是Actor模型的隐式实现,对应关系不像Akka那样明显,不过本质上也是一种Actor模型。

Actor可以创建新的Actor,这些Actor最终会呈现出一个树状结构,非常像现实世界里的组织结构,所以利用Actor模型来对程序进行建模,和现实世界的匹配度非常高。

Actor模型和现实世界一样都是异步模型,理论上不保证消息百分百送达,也不保证消息送达的顺序和发送的顺序是一致的,甚至无法保证消息会被百分百处理。

虽然实现Actor模型的厂商都在试图解决这些问题,但遗憾的是解决得并不完美,所以使用Actor模型也是有成本的。

  • 消息和对象方法的区别

Actor内部有一个邮箱(Mailbox)
接收到的消息都是先放到邮箱里,如果邮箱里有积压的消息,那么新收到的消息就不会马上得到处理
也正是因为Actor使用单线程处理消息,所以不会出现并发问题
你可以把Actor内部的工作模式想象成只有一个消费者线程的生产者-消费者模式
所以,在Actor模型里,发送消息仅仅是把消息发出去而已,接收消息的Actor在接收到消息后,也不一定会立即处理

也就是说Actor中的消息机制完全是异步的。而调用对象方法,实际上是同步的,对象方法return之前,调用方会一直等待

除此之外,调用对象方法,需要持有对象的引用,所有的对象必须在同一个进程中
而在Actor中发送消息,类似于现实中的写信,只需要知道对方的地址就可以,发送消息和接收消息的Actor可以不在一个进程中,也可以不在同一台机器上。因此,Actor模型不但适用于并发计算,还适用于分布式计算。

  • 案例:实现累加器
//累加器
static class CounterActor extends UntypedActor {
    private int counter = 0;
    @Override
    public void onReceive(Object message){
        //如果接收到的消息是数字类型,执⾏累加操作,
		//否则打印counter的值
        if (message instanceof Number) {
            counter += ((Number) message).intValue();
        } else {
            System.out.println(counter);
        }
    }
}
public static void main(String[] args) throws InterruptedException {
    //创建Actor系统
    ActorSystem system = ActorSystem.create("HelloSystem");
    //4个线程⽣产消息
    ExecutorService es = Executors.newFixedThreadPool(4);
    //创建CounterActor
    ActorRef counterActor = system.actorOf(Props.create(CounterActor.class));
    //⽣产4*100000个消息
    for (int i = 0; i < 4; i++) {
        es.execute(() -> {
            for (int j = 0; j < 100000; j++) {
                //发送1这个值给onReceive
                counterActor.tell(1, ActorRef.noSender());
            }
        });
    }
    //关闭线程池
    es.shutdown();
    //等待CounterActor处理完所有消息
    Thread.sleep(1000);
    //打印结果
    counterActor.tell("", ActorRef.noSender());
    //关闭Actor系统
    system.shutdown();
}

2 软件事务内存:借鉴数据库的并发经验

使用MVCC实现的STM来实现线程安全

手写STM:操作的对象类、当前事务类、操作当前事务的类、实现方法类

VersionedRef
作用:将对象value包装成带版本号的对象;
按照MVCC理论,数据的每一次修改都对应着一个唯一的版本号,所以不存在仅仅改变value或者version的情况,用不变性模式就可以很好地解决这个问题,所以VersionedRef这个类被我们设计成了不可变的

//带版本号的对象引用
public final class VersionedRef<T> {
    final T value;
    final long version;
    //构造方法
    public VersionedRef(T value, long version) {
        this.value = value;
        this.version = version;
    }
}

TxnRef:
作用:负责完成事务内的读写操作(所有对数据的读写操作,一定是在一个事务里面)
读写操作委托给了接口Txn

//支持事务的引用
public class TxnRef<T> {
    //当前数据,带版本号
    volatile VersionedRef curRef;
    //构造方法
    public TxnRef(T value) {
        this.curRef = new VersionedRef(value, 0L);
    }
    //获取当前事务中的数据
    public T getValue(Txn txn) {
        return txn.get(this);
    }
    //在当前事务中设置数据
    public void setValue(T value, Txn txn) {
        txn.set(this, value);
    }
}

Txn:
TxnRef的读写操作委托给了接口Txn,Txn代表的是读写操作所在的当前事务, 内部持有的curRef代表的
是系统中的最新值

public interface Txn {
    <T> T get(TxnRef<T> ref);
    <T> void set(TxnRef<T> ref, T value);
}

STMTxn:

//事务接口
//STM事务实现类
public final class STMTxn implements Txn {
    //事务ID生成器
    private static AtomicLong txnSeq = new AtomicLong(0);
    //当前事务所有的相关数据(保存当前事务中所有读写的数据的快照)
    private Map<TxnRef, VersionedRef> inTxnMap = new HashMap<>();
    //当前事务所有需要修改的数据(保存当前事务需要写入的数据)
    private Map<TxnRef, Object> writeMap = new HashMap<>();
    //当前事务ID
    private long txnId;

    //构造函数,自动生成当前事务ID
    STMTxn() {
        txnId = txnSeq.incrementAndGet();
    }

    //获取当前事务中的数据:
    //将需要读取的数据加入inTxnMap,同时保证每次读取的数据都是一个版本
    @Override
    public <T> T get(TxnRef<T> ref) {
        if (!inTxnMap.containsKey(ref)) {
            inTxnMap.put(ref, ref.curRef);
        }
        return (T) inTxnMap.get(ref).value;
    }

    //在当前事务中修改数据
    //将要写入的数据放入writeMap,但如果写入的数据没被读取过,也会将其放入 inTxnMap
    @Override
    public <T> void set(TxnRef<T> ref, T value) {
        if (!inTxnMap.containsKey(ref)) {
            inTxnMap.put(ref, ref.curRef);
        }
        writeMap.put(ref, value);
    }

    //提交事务
    //为了简化实现,使用了互斥锁,所以事务的提交是串行的
    //首先检查inTxnMap中的数据是否发生过变化,如果没有发生变化,那么就将writeMap中的数据写入(这里的写入其实就是TxnRef内部持有的curRef);如果发生过变化,那么就不能将writeMap中的数据写入了
    boolean commit() {
        synchronized (STM.commitLock) {
            //是否校验通过
            boolean isValid = true;
            //校验所有读过的数据是否发生过变化
            for (Map.Entry<TxnRef, VersionedRef> entry : inTxnMap.entrySet()) {
                VersionedRef curRef = entry.getKey().curRef;
                VersionedRef readRef = entry.getValue();
                //通过版本号,即事务id来验证数据是否发生过变化
                if (curRef.version != readRef.version) {
                    isValid = false;
                    break;
                }
            }
            //如果校验通过,则所有更改生效
            if (isValid) {
                writeMap.forEach((k, v) -> {k.curRef = new VersionedRef(v, txnId);
                });
            }
            return isValid;
        }
    }
}

实现Multiverse中的原子化操作atomic():使用了类似于CAS的操作,如果事务提交失败,那么就重新创建一个新的事务,重新执行。

@FunctionalInterface
public interface TxnRunnable {
    void run(Txn txn);
}
//STM
public final class STM {
    //私有化构造方法
    private STM() {
        //提交数据需要用到的全局锁
        static final Object commitLock = new Object();
        //原子化提交方法
        public static void atomic (TxnRunnable action){
            boolean committed = false;
            //如果没有提交成功,则一直重试
            while (!committed) {
                //创建新的事务
                STMTxn txn = new STMTxn();
                //执行业务逻辑,由使用者编写
                action.run(txn);
                //提交事务
                committed = txn.commit();
            }
        }
    }
}

使用:

class Account {
    //余额
    private TxnRef<Integer> balance;
    //构造方法
    public Account(int balance) {
        this.balance = new TxnRef<Integer>(balance);
    }
    //转账操作
    public void transfer(Account target, int amt){
        STM.atomic((txn)->{
            Integer from = balance.getValue(txn);
            balance.setValue(from-amt, txn);
            Integer to = target.balance.getValue(txn);
            target.balance.setValue(to+amt, txn);
        });
    }
}

如果想增加方法,直接在STM类里,仿照atomic方法进行编写业务即可

3 协程:更轻量级的线程

可以把协程简单地理解为一种轻量级的线程。
从操作系统的角度来看,线程是在内核态中调度的,而协程是在用户态调度的,所以相对于线程来说,协程切换的成本更低。
协程虽然也有自己的栈,但是相比线程栈要小得多,典型的线程栈大小差不多有1M,而协程栈的大小往往只有几K或者几十K。所以,无论是从时间维度还是空间维度来看,协程都比线程轻量得多。

4 CSP模型:Golang的主力队员

Golang解决协程协作问题的两种不同的方案:
一种方案支持协程之间以共享内存的方式通信,Golang提供了管程和原子类来对协程进行同步控制,这个方案与Java语言类似;
另一种方案支持协程之间以消息传递(Message-Passing)的方式通信,本质上是要避免共享,Golang的这个方案是基于CSP(CommunicatingSequential Processes)模型实现的,比较推荐使用

“不要以共享内存方式通信,要以通信方式共享内存”,即方案二

CSP模型类似生产者-消费者模型,其中的队列概念可以用channel来实现
容量为0的channel在Golang中被称为无缓冲的channel,容量大于0的则被称为有缓冲的channel
无缓冲的channel类似于Java中提供的SynchronousQueue,主要用途是在两个协程之间做数据交换

  • 与Actor对比

相同:以消息传递的方式来避免共享

不同:
第一个最明显的区别就是:Actor模型中没有channel。虽然Actor模型中的 mailbox 和 channel 非常像,看上去都像个FIFO队列,但是区别还是很大的。Actor模型中的mailbox对于程序员来说是“透明”的,mailbox明确归属于一个特定的Actor,是Actor模型中的内部机制;而且Actor之间是可以直接通信的,不需要通信中介。但CSP模型中的 channel 就不一样了,它对于程序员来说是“可见”的,是通信的中介,传递的消息都是直接发送到 channel 中的。
第二个区别是:Actor模型中发送消息是非阻塞的,而CSP模型中是阻塞的。Golang实现的CSP模型,channel是一个阻塞队列,当阻塞队列已满的时候,向channel中发送数据,会导致发送消息的协程阻塞。
第三个区别则是关于消息送达的。Actor模型理论上不保证消息百分百送达,而在Golang实现的CSP模型中,是能保证消息百分百送达的。不过这种百分百送达也是有代价的,那就是有可能会导致死锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值