<commit_before>---
layout: post
title: "Akka系列（四）：Akka中的共享内存模型"
modified:
categories: blog
excerpt:
tags: []
image:
  feature:
date: 2017-05-01
---

通过前几篇的学习，相信大家对Akka应该有所了解了，都说解决并发哪家强，JVM上面找Akka，那么Akka到底在解决并发问题上帮我们做了什么呢？


## 共享内存

众所周知，在处理并发问题上面，最核心的一部分就是如何处理共享内存，很多时候我们都需要花费很多时间和精力在共享内存上，那么在学习Akka对共享内存是如何管理之前，我们先来看看Java中是怎么处理这个问题的。

### Java共享内存

相信对Java并发有所了解的同学都应该知道在Java5推出JSR 133后，Java对内存管理有了更高标准的规范了，这使我们开发并发程序也有更好的标准了，不会有一些模糊的定义导致的无法确定的错误。

首先来看看一下Java内存模型的简单构图：

![Java Memory](/images/java-memory.png)

从图中我们可以看到我们线程都有自己的一个工作内存，这就好比高速缓存，它是对主内存部分数据的拷贝，线程对自己工作内存的操作速度远远快于对主内存的操作，但这也往往会引起共享变量不一致的问题，比如以下一个场景：

```java
int a = 0;
public void setA() {
  a = a + 1;
}
```
上面是一个很简单的例子，a是一个全局变量，然后我们有一个方法去修改这个值，每次增加一，假如我们用100个线程去运行这段代码，那a最终的结果会是多少呢？
100？显然不一定，它可能是80，90，或者其他数，这就造成共享变量不一致的问题，那么为什么会导致这个问题呢，就是我们上面所说的，线程去修改a的时候可能就只是修改了自己工作内存中a的副本，但并没有将a的值及时的刷新到主内存中，这便会导致其他线程可能读到未被修改a的值，最终出现变量不一致问题。

那么Java中是怎么处理这种问题，如何保证共享变量的一致性的呢？

#### 同步机制

大体上Java中有3类同步机制，但它们所解决的问题并不相同，我们先来看一看这三种机制：

- final关键词
- volatile关键词
- synchronized关键词（这里代表了所有类似监视锁的机制）

##### 1.final关键词

写过Java程序的同学对这个关键词应该再熟悉不过了，其基本含义就是**不可变**，不可变变量，比如：

```java
final int a = 10;
final String b = "hello";
```
不可变的含义在于当你对这些变量或者对象赋初值后，不能再重新去赋值，但对于对象来说，我们不能修改的是它的引用，但是对象内的内容还是可以修改的。下面是一个简单的例子：

```java
final User u = new User(1,"a");
u.id = 2; //可以修改
u = new User(2,"b"); //不可修改
```
所以在利用final关键词用来保证共享变量的一致性时一定要了解清楚自己的需求，选择合适的方法。

#### 3.volatile关键词

很多同学在遇到共享变量不一致的问题后，都会说我在声明变量前加一个volatile就好了，但事实真是这样嘛？答案显然不是。那我们来看看volatile到底为我们做了什么。

前面我们说过每个线程都有自己的工作内存，很多时候线程去修改一个变量的值只是修改了自己工作内存中副本的值，这便会导致主内存的值并不是最新的，其他线程读取到的变量便会出现问题。volatile帮我们解决了这个问题，它有两个特点：

- 线程每次都会去主内存中读取变量
- 线程每次修改变量后的值都会及时更新到主内存中去

举个例子：

```java
volatile int a = 0;
public void setA() {
  a = a + 1;
}
```
现在线程在执行这段代码时，都会强制去主内存中读取变量的值，修改后也会马上更新到主内存中去，但是这真的能解决共享变量不一致的问题嘛，其实不然，比如我们有这么一个场景：两个线程同时读取了主内存中变量最新的值，这是我们两个线程都去执行修改操作，最后结果会是什么呢？这里就留给大家自己去思考了，其实也很简单的。

那么volatile在什么场景下能保证线程安全，按照官方来说，有以下两个条件：

- 对变量的写操作不依赖于当前值
- 该变量没有包含在具有其他变量的不变式中

多的方面这里我就不展开了，推荐两篇我觉得写的还不错的文章：[volatile的使用及其原理](http://www.cnblogs.com/paddix/p/5428507.html)[volatile的适用场景](http://blog.csdn.net/vking_wang/article/details/9982709)

#### 2.synchronized关键词

很多同学在学习Java并发过程中最先接触的就是synchronized关键词了，它确实能解决我们上述的并发问题，那它到时如何帮我们保证共享变量的一致性的呢？

简而言之的说，线程在访问请求用synchronized关键词修饰的方法，代码块都会要求获得一个监视器锁，当线程获得了监视器锁后，它才有权限去执行相应的方法或代码块，并在执行结束后释放监视器锁，这便能保证共享内存的一致性了，因为本文主要是讲Akka的共享内存，过多的篇幅就不展开了，这里推荐一篇解析synchronized原理很不错的文章，有兴趣的同学可以去看看：[Synchronized及其实现原理](http://www.cnblogs.com/paddix/p/5367116.html)


### Akka共享内存

Akka中的共享内存是基于Actor模型的，Actor模型提倡的是：**通过通讯来实现共享内存，而不是用共享内存来实现通讯**，这点是跟Java解决共享内存最大的区别，举个例子：
在Java中我们要去操作共享内存中数据时，每个线程都需要不断的获取共享内存的监视器锁，然后将操作后的数据暴露给其他线程访问使用，用共享内存来实现各个线程之间的通讯，而在Akka中我们可以将共享可变的变量作为一个Actor内部的状态，利用Actor模型本身串行处理消息的机制来保证变量的一致性。

当然要使用Akka中的机制也必须满足一下两条原则：

- 消息的发送必须先于消息的接收
- 同一个Actor对一条消息的处理先于下一条消息处理

第二个原则很好理解，就是上面我们说的Actor内部是串行处理消息，那我们来看看第一个原则，为什么要保证消息的发送先于消息的接收，是为了防止我们在创建消息的时候发生了不确定的错误，接收者将可能接收到不正确的消息，导致发生奇怪的异常。

通过前面的学习我们知道Actor是一种比线程更轻量级，抽象程度更高的一种结构，它帮我们规避了我们自己去操作线程，那么Akka底层到底是怎么帮我们去保证共享内存的一致性的呢？

一个Actor它可能会有很多线程同时向它发送消息，之前我们也说到Actor本身是串行处理的消息的，那它是如何保障这种机制的呢？

#### Mailbox 

Mailbox在Actor模型是一个很重要的概念，我们都知道向一个Actor发送的消息首先都会被存储到它所对应的Mailbox中，那么我们先来看看MailBox的定义结构(本文所引用的代码都在akka.dispatch.Mailbox.scala中，有兴趣的同学也可以去研究一下）：

```scala
private[akka] abstract class Mailbox(val messageQueue: MessageQueue)
  extends ForkJoinTask[Unit] with SystemMessageQueue with Runnable {}<commit_msg>add akka-4<commit_after>---
layout: post
title: "Akka系列（四）：Akka中的共享内存模型"
modified:
categories: blog
excerpt:
tags: []
image:
  feature:
date: 2017-05-01
---

通过前几篇的学习，相信大家对Akka应该有所了解了，都说解决并发哪家强，JVM上面找Akka，那么Akka到底在解决并发问题上帮我们做了什么呢？


## 共享内存

众所周知，在处理并发问题上面，最核心的一部分就是如何处理共享内存，很多时候我们都需要花费很多时间和精力在共享内存上，那么在学习Akka对共享内存是如何管理之前，我们先来看看Java中是怎么处理这个问题的。

### Java共享内存

相信对Java并发有所了解的同学都应该知道在Java5推出JSR 133后，Java对内存管理有了更高标准的规范了，这使我们开发并发程序也有更好的标准了，不会有一些模糊的定义导致的无法确定的错误。

首先来看看一下Java内存模型的简单构图：

![Java Memory](/images/java-memory.png)

从图中我们可以看到我们线程都有自己的一个工作内存，这就好比高速缓存，它是对主内存部分数据的拷贝，线程对自己工作内存的操作速度远远快于对主内存的操作，但这也往往会引起共享变量不一致的问题，比如以下一个场景：

```java
int a = 0;
public void setA() {
  a = a + 1;
}
```
上面是一个很简单的例子，a是一个全局变量，然后我们有一个方法去修改这个值，每次增加一，假如我们用100个线程去运行这段代码，那a最终的结果会是多少呢？
100？显然不一定，它可能是80，90，或者其他数，这就造成共享变量不一致的问题，那么为什么会导致这个问题呢，就是我们上面所说的，线程去修改a的时候可能就只是修改了自己工作内存中a的副本，但并没有将a的值及时的刷新到主内存中，这便会导致其他线程可能读到未被修改a的值，最终出现变量不一致问题。

那么Java中是怎么处理这种问题，如何保证共享变量的一致性的呢？

#### 同步机制

大体上Java中有3类同步机制，但它们所解决的问题并不相同，我们先来看一看这三种机制：

- final关键词
- volatile关键词
- synchronized关键词（这里代表了所有类似监视锁的机制）

##### 1.final关键词

写过Java程序的同学对这个关键词应该再熟悉不过了，其基本含义就是**不可变**，不可变变量，比如：

```java
final int a = 10;
final String b = "hello";
```
不可变的含义在于当你对这些变量或者对象赋初值后，不能再重新去赋值，但对于对象来说，我们不能修改的是它的引用，但是对象内的内容还是可以修改的。下面是一个简单的例子：

```java
final User u = new User(1,"a");
u.id = 2; //可以修改
u = new User(2,"b"); //不可修改
```
所以在利用final关键词用来保证共享变量的一致性时一定要了解清楚自己的需求，选择合适的方法。

#### 2.volatile关键词

很多同学在遇到共享变量不一致的问题后，都会说我在声明变量前加一个volatile就好了，但事实真是这样嘛？答案显然不是。那我们来看看volatile到底为我们做了什么。

前面我们说过每个线程都有自己的工作内存，很多时候线程去修改一个变量的值只是修改了自己工作内存中副本的值，这便会导致主内存的值并不是最新的，其他线程读取到的变量便会出现问题。volatile帮我们解决了这个问题，它有两个特点：

- 线程每次都会去主内存中读取变量
- 线程每次修改变量后的值都会及时更新到主内存中去

举个例子：

```java
volatile int a = 0;
public void setA() {
  a = a + 1;
}
```
现在线程在执行这段代码时，都会强制去主内存中读取变量的值，修改后也会马上更新到主内存中去，但是这真的能解决共享变量不一致的问题嘛，其实不然，比如我们有这么一个场景：两个线程同时读取了主内存中变量最新的值，这是我们两个线程都去执行修改操作，最后结果会是什么呢？这里就留给大家自己去思考了，其实也很简单的。

那么volatile在什么场景下能保证线程安全，按照官方来说，有以下两个条件：

- 对变量的写操作不依赖于当前值
- 该变量没有包含在具有其他变量的不变式中

多的方面这里我就不展开了，推荐两篇我觉得写的还不错的文章：[volatile的使用及其原理](http://www.cnblogs.com/paddix/p/5428507.html)[volatile的适用场景](http://blog.csdn.net/vking_wang/article/details/9982709)

#### 3.synchronized关键词

很多同学在学习Java并发过程中最先接触的就是synchronized关键词了，它确实能解决我们上述的并发问题，那它到时如何帮我们保证共享变量的一致性的呢？

简而言之的说，线程在访问请求用synchronized关键词修饰的方法，代码块都会要求获得一个监视器锁，当线程获得了监视器锁后，它才有权限去执行相应的方法或代码块，并在执行结束后释放监视器锁，这便能保证共享内存的一致性了，因为本文主要是讲Akka的共享内存，过多的篇幅就不展开了，这里推荐一篇解析synchronized原理很不错的文章，有兴趣的同学可以去看看：[Synchronized及其实现原理](http://www.cnblogs.com/paddix/p/5367116.html)


### Akka共享内存

Akka中的共享内存是基于Actor模型的，Actor模型提倡的是：**通过通讯来实现共享内存，而不是用共享内存来实现通讯**，这点是跟Java解决共享内存最大的区别，举个例子：
在Java中我们要去操作共享内存中数据时，每个线程都需要不断的获取共享内存的监视器锁，然后将操作后的数据暴露给其他线程访问使用，用共享内存来实现各个线程之间的通讯，而在Akka中我们可以将共享可变的变量作为一个Actor内部的状态，利用Actor模型本身串行处理消息的机制来保证变量的一致性。

当然要使用Akka中的机制也必须满足一下两条原则：

- 消息的发送必须先于消息的接收
- 同一个Actor对一条消息的处理先于下一条消息处理

第二个原则很好理解，就是上面我们说的Actor内部是串行处理消息，那我们来看看第一个原则，为什么要保证消息的发送先于消息的接收，是为了防止我们在创建消息的时候发生了不确定的错误，接收者将可能接收到不正确的消息，导致发生奇怪的异常。

通过前面的学习我们知道Actor是一种比线程更轻量级，抽象程度更高的一种结构，它帮我们规避了我们自己去操作线程，那么Akka底层到底是怎么帮我们去保证共享内存的一致性的呢？

一个Actor它可能会有很多线程同时向它发送消息，之前我们也说到Actor本身是串行处理的消息的，那它是如何保障这种机制的呢？

#### Mailbox 

Mailbox在Actor模型是一个很重要的概念，我们都知道向一个Actor发送的消息首先都会被存储到它所对应的Mailbox中，那么我们先来看看MailBox的定义结构(本文所引用的代码都在akka.dispatch.Mailbox.scala中，有兴趣的同学也可以去研究一下）：

```scala
private[akka] abstract class Mailbox(val messageQueue: MessageQueue)
  extends ForkJoinTask[Unit] with SystemMessageQueue with Runnable {}
{'fraction_non_alphanumeric': 0.10060214422088412, 'fraction_numerical': 0.01659568218534293, 'mean_word_length': 11.971428571428572, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 4, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6517458', 'n_tokens_mistral': 5435, 'n_tokens_neox': 6116, 'n_words': 235}