线程 - Java SDK
为了使您的 Android 应用快速运行且响应迅速,您必须在布局视觉效果和处理用户交互所需的计算时间与处理数据和运行业务逻辑所需的时间之间取得平衡。 通常,开发者会将这项工作分散到多个线程中:主线程或用户界面线程用于所有与用户界面相关的工作,一个或多个背景线程用于计算较重的工作负载,然后再将其发送到用户界面线程进行演示。通过将繁重的工作卸载给背景线程,无论工作负载有多大,用户界面线程都可以保持快速响应。
要记住的三项规则
当您遵循以下三个规则时,Realm 可以启用简单且安全的多线程代码:
- 如果在背景线程上进行写入,请避免在用户界面线程上进行写入:
- 您可以从任何线程写入域 ,但一次只能有一个写入者。 因此,写入事务会相互区块。 用户界面线程上的写入操作可能会导致应用在等待背景线程上的写入操作完成时显示为无响应。 如果您使用的是 Sync ,请避免在用户界面线程上进行写入,因为 Sync 会在背景线程上进行写入。
- 不要将活动对象、集合或 Realm 传递给其他线程:
- 活动对象、集合和 Realm 实例具有线程限制:换言之,它们仅在创建它们的线程上有效。 实际上,这意味着您不能将活动实例传递给其他线程。 但是,Realm 提供了几种跨线程共享对象的机制。
- 无需锁即可读取:
- Realm 的多版本并发控制 (MVCC)架构消除了对读取操作使用锁的需要。 您读取的值永远不会损坏或处于部分修改状态。 您可以在任何线程上自由地从 Realm 中读取数据,而无需使用锁或互斥锁。 不必要的锁定会成为性能瓶颈,因为每个线程在读取之前都可能需要等待。
跨线程通信
活动对象、collection和Realm具有线程限制。如果需要在多个线程中处理相同的数据,则应在多个线程上将同一 Realm 作为单独的 Realm 实例打开。 Java SDK会尽可能整合跨线程的底层连接,从而提高此模式的效率。
当您需要跨线程通信时,根据您的具体用例,您有多种选择:
要修改两个线程上的数据,请使用 主键 在两个线程上 查询 该对象。
要将对象的快速只读视图发送到其他线程,请冻结该对象。
要在应用中保留并共享对象的多个只读视图,请从 Realm 复制该对象。
要对任何线程上所做的更改React,请使用通知。
要在当前线程上查看 Realm 中其他线程的更改,请刷新Realm 实例(事件循环线程会自动刷新)。
意图
托管 RealmObject
实例不是线程安全的,也不是Parcelable
,因此您不能通过Intent
在活动或线程之间传递它们。 相反,您可以在Intent
extras 捆绑包中传递 ObjectId(例如主键),然后在单独的线程中打开一个新的 Realm 实例来查询该标识符。 或者,您可以冻结Realm 对象。
冻结对象
在大多数情况下,具有线程限制的活动对象都可以正常工作。 但是,某些应用(例如基于事件流的响应式架构的应用)需要跨线程发送不可变副本。 在这种情况下,您可以冻结对象、collection和 Realm。
冻结操作会创建特定对象、collection或 Realm 的不可变视图,该视图仍存在于磁盘上,并且在传递给其他线程时无需进行深度复制。您可以跨线程随意共享冻结对象,而不必担心线程问题。
冻结对象不是活动对象,也不会自动更新。 它们实际上是冻结时对象状态的快照。冻结 Realm 时,所有子对象和集合也会被冻结。 您无法修改冻结的对象,但可以从冻结的对象读取主键,在活动的域中查询底层的对象,然后更新该活动的对象实例。
只要生成冻结对象的 域 保持打开状态,冻结对象就会保持有效。在所有线程都完成对这些冻结对象的操作之前,避免关闭包含冻结对象的 Realm。
警告
冻结对象异常
使用冻结对象时,尝试执行以下任一操作都会引发异常:
在冻结的 Realm 中启动写事务。
修改冻结对象。
将变更侦听器添加到冻结的 Realm、集合或对象中。
对象一旦冻结,就无法解冻。 您可以使用isFrozen()
检查对象是否被冻结。 此方法始终是线程安全的。
要冻结对象、集合或 Realm,请使用Freeze()方法:
Realm realm = Realm.getInstance(config); // Get an immutable copy of the realm that can be passed across threads Realm frozenRealm = realm.freeze(); Assert.assertTrue(frozenRealm.isFrozen()); RealmResults<Frog> frogs = realm.where(Frog.class).findAll(); // You can freeze collections RealmResults<Frog> frozenFrogs = frogs.freeze(); Assert.assertTrue(frozenFrogs.isFrozen()); // You can still read from frozen realms RealmResults<Frog> frozenFrogs2 = frozenRealm.where(Frog.class).findAll(); Assert.assertTrue(frozenFrogs2.isFrozen()); Frog frog = frogs.first(); Assert.assertTrue(!frog.getRealm().isFrozen()); // You can freeze objects Frog frozenFrog = frog.freeze(); Assert.assertTrue(frozenFrog.isFrozen()); // Frozen objects have a reference to a frozen realm Assert.assertTrue(frozenFrog.getRealm().isFrozen());
val realm = Realm.getInstance(config) // Get an immutable copy of the realm that can be passed across threads val frozenRealm = realm.freeze() Assert.assertTrue(frozenRealm.isFrozen) val frogs = realm.where(Frog::class.java).findAll() // You can freeze collections val frozenFrogs = frogs.freeze() Assert.assertTrue(frozenFrogs.isFrozen) // You can still read from frozen realms val frozenFrogs2 = frozenRealm.where(Frog::class.java).findAll() Assert.assertTrue(frozenFrogs2.isFrozen) val frog: Frog = frogs.first()!! Assert.assertTrue(!frog.realm.isFrozen) // You can freeze objects val frozenFrog: Frog = frog.freeze() Assert.assertTrue(frozenFrog.isFrozen) Assert.assertTrue(frozenFrog.realm.isFrozen)
重要
冻结对象和 Realm 大小
冻结对象会保留它们被冻结时包含它们的 Realm 的完整副本。 因此,与没有冻结对象的情况相比,冻结大量对象可能会导致域消耗更多的内存和存储空间。如果您需要长时间单独冻结大量对象,请考虑将所需对象复制到域之外。
刷新 Realm
当您打开一个 Realm 时,它会反映最近成功的写入提交,并保留在该版本上直到刷新。 这意味着该 Realm 在下次刷新之前不会看到另一个线程上发生的更改。 任何事件循环线程(包括用户界面线程)上的 Realm 都会在该线程循环开始时自动刷新自身。 但是,您必须手动刷新与非循环线程绑定或禁用自动刷新的 Realm 实例。 要刷新 Realm,请调用Realm.refresh():
if (!realm.isAutoRefresh()) { // manually refresh realm.refresh(); }
if (!realm.isAutoRefresh) { // manually refresh realm.refresh() }
提示
写入时刷新
Realm 还会在完成写事务(write transaction)后自动刷新。
Realm 的线程模型深入剖析
Realm 通过其多版本并发控制 (MVCC) 架构提供安全、快速、无锁和跨线程的并发访问。
与 Git 的比较和对比
如果您熟悉 Git 这样的分布式版本控制系统 ,那么您可能已经对 MVCC 有了直观的理解。Git 的两个基本要素如下:
提交,即原子写入。
分支,即不同版本的提交历史记录。
与此类似,Realm 也采用事务的形式进行原子提交的写入。Realm 在任何时候都有许多不同的历史版本,比如分支。
与通过分叉积极支持分发和发散的 Git 不同,域在任何给定时间只有一个真正的最新版本,并且始终会写入到该最新版本的头部。Realm 无法写入以前的版本。 这是有道理的:您的数据应该收敛于最新的事实版本。
内部结构
域是使用 B+ 树数据结构实现的。顶级节点代表域的一个版本;子节点是该域版本中的对象。该域有一个指向其最新版本的指针,就像 Git 有一个指向其 HEAD 提交的指针一样。
Realm 使用写时复制技术来确保 隔离 性 和 持久性 。当您进行更改时,Realm 会复制树的相关部分以进行写入,然后分两个阶段提交更改:
将更改写入磁盘并验证是否成功。
将最新版本指针设置为点新编写的版本。
这种两步提交过程可保证:即使写入中途失败,原始版本也不会以任何方式损坏。这是因为变更是对树的相关部分的副本进行的。同样,Realm 的根指针将指向原始版本,直到保证新版本有效为止。
例子
下图说明了提交过程:
Realm 采用树的结构。Realm 有一个指向其最新版本 V1 的指针。
在写入时,Realm 会创建一个基于 V1 的新版本 V2。 Realm 创建对象的副本以进行个性(A 1 、 C 1 ),而指向未修改对象的链接将继续指向原始版本(B、D)。
验证提交后,Realm 会将指针更新到最新版本 V2。然后,Realm 会丢弃不再与树连接的旧节点。
Realm 使用内存映射之类的零拷贝技术来处理数据。当您从 Realm 读取值时,您其实是在实际磁盘(而非其副本)上查看该值。这是活动对象的基础。也正是因为如此,系统才能在磁盘写入验证完成后将 Realm 头部指针设置为指向新版本。
总结
遵循以下三个规则时,Realm 可以启用简单且安全的多线程代码:
如果对后台线程进行写入或使用Sync,请避免对 UI 线程进行写入。
请勿将实时对象传递给其他线程。
不要锁读取。
为了查看在您 Realm 实例中其他线程上所进行的变更,您必须手动刷新“循环”线程上不存在的 Realm 实例或禁用自动刷新的 Realm 实例。
对于基于事件的响应式架构的应用,您可以冻结对象、collection和 realms,以便高效地将副本传递到不同的线程进行处理。
Realm 的多版本并发控制 (MVCC) 架构与 Git 类似。与 Git 不同,Realm 对于每个 Realm 实例只有一个真正的最新版本。
Realm 分两个阶段进行提交以保证隔离性和耐久性。