|
13 | 13 |
|
14 | 14 | package io.reactivex.subjects;
|
15 | 15 |
|
16 |
| -import io.reactivex.annotations.Nullable; |
17 | 16 | import java.lang.reflect.Array;
|
18 | 17 | import java.util.*;
|
19 | 18 | import java.util.concurrent.TimeUnit;
|
20 | 19 | import java.util.concurrent.atomic.*;
|
21 | 20 |
|
22 | 21 | import io.reactivex.Observer;
|
23 | 22 | import io.reactivex.Scheduler;
|
24 |
| -import io.reactivex.annotations.CheckReturnValue; |
| 23 | +import io.reactivex.annotations.*; |
25 | 24 | import io.reactivex.disposables.Disposable;
|
26 | 25 | import io.reactivex.internal.functions.ObjectHelper;
|
27 | 26 | import io.reactivex.internal.util.NotificationLite;
|
|
94 | 93 | * in a non-blocking and thread-safe manner via {@link #hasValue()}, {@link #getValue()},
|
95 | 94 | * {@link #getValues()} or {@link #getValues(Object[])}.
|
96 | 95 | * <p>
|
97 |
| - * Note that due to concurrency requirements, a size-bounded {@code ReplaySubject} may hold strong references to more |
98 |
| - * source emissions than specified. |
| 96 | + * Note that due to concurrency requirements, a size- and time-bounded {@code ReplaySubject} may hold strong references to more |
| 97 | + * source emissions than specified while it isn't terminated yet. Use the {@link #cleanupBuffer()} to allow |
| 98 | + * such inaccessible items to be cleaned up by GC once no consumer references it anymore. |
99 | 99 | * <dl>
|
100 | 100 | * <dt><b>Scheduler:</b></dt>
|
101 | 101 | * <dd>{@code ReplaySubject} does not operate by default on a particular {@link io.reactivex.Scheduler} and
|
@@ -415,6 +415,24 @@ public T getValue() {
|
415 | 415 | return buffer.getValue();
|
416 | 416 | }
|
417 | 417 |
|
| 418 | + /** |
| 419 | + * Makes sure the item cached by the head node in a bounded |
| 420 | + * ReplaySubject is released (as it is never part of a replay). |
| 421 | + * <p> |
| 422 | + * By default, live bounded buffers will remember one item before |
| 423 | + * the currently receivable one to ensure subscribers can always |
| 424 | + * receive a continuous sequence of items. A terminated ReplaySubject |
| 425 | + * automatically releases this inaccessible item. |
| 426 | + * <p> |
| 427 | + * The method must be called sequentially, similar to the standard |
| 428 | + * {@code onXXX} methods. |
| 429 | + * @since 2.1.11 - experimental |
| 430 | + */ |
| 431 | + @Experimental |
| 432 | + public void cleanupBuffer() { |
| 433 | + buffer.trimHead(); |
| 434 | + } |
| 435 | + |
418 | 436 | /** An empty array to avoid allocation in getValues(). */
|
419 | 437 | private static final Object[] EMPTY_ARRAY = new Object[0];
|
420 | 438 |
|
@@ -563,6 +581,12 @@ interface ReplayBuffer<T> {
|
563 | 581 | * @return true if successful
|
564 | 582 | */
|
565 | 583 | boolean compareAndSet(Object expected, Object next);
|
| 584 | + |
| 585 | + /** |
| 586 | + * Make sure an old inaccessible head value is released |
| 587 | + * in a bounded buffer. |
| 588 | + */ |
| 589 | + void trimHead(); |
566 | 590 | }
|
567 | 591 |
|
568 | 592 | static final class ReplayDisposable<T> extends AtomicInteger implements Disposable {
|
@@ -619,10 +643,16 @@ public void add(T value) {
|
619 | 643 | @Override
|
620 | 644 | public void addFinal(Object notificationLite) {
|
621 | 645 | buffer.add(notificationLite);
|
| 646 | + trimHead(); |
622 | 647 | size++;
|
623 | 648 | done = true;
|
624 | 649 | }
|
625 | 650 |
|
| 651 | + @Override |
| 652 | + public void trimHead() { |
| 653 | + // no-op in this type of buffer |
| 654 | + } |
| 655 | + |
626 | 656 | @Override
|
627 | 657 | @Nullable
|
628 | 658 | @SuppressWarnings("unchecked")
|
@@ -839,9 +869,24 @@ public void addFinal(Object notificationLite) {
|
839 | 869 | size++;
|
840 | 870 | t.lazySet(n); // releases both the tail and size
|
841 | 871 |
|
| 872 | + trimHead(); |
842 | 873 | done = true;
|
843 | 874 | }
|
844 | 875 |
|
| 876 | + /** |
| 877 | + * Replace a non-empty head node with an empty one to |
| 878 | + * allow the GC of the inaccessible old value. |
| 879 | + */ |
| 880 | + @Override |
| 881 | + public void trimHead() { |
| 882 | + Node<Object> h = head; |
| 883 | + if (h.value != null) { |
| 884 | + Node<Object> n = new Node<Object>(null); |
| 885 | + n.lazySet(h.get()); |
| 886 | + head = n; |
| 887 | + } |
| 888 | + } |
| 889 | + |
845 | 890 | @Override
|
846 | 891 | @Nullable
|
847 | 892 | @SuppressWarnings("unchecked")
|
@@ -1047,12 +1092,24 @@ void trimFinal() {
|
1047 | 1092 | for (;;) {
|
1048 | 1093 | TimedNode<Object> next = h.get();
|
1049 | 1094 | if (next.get() == null) {
|
1050 |
| - head = h; |
| 1095 | + if (h.value != null) { |
| 1096 | + TimedNode<Object> lasth = new TimedNode<Object>(null, 0L); |
| 1097 | + lasth.lazySet(h.get()); |
| 1098 | + head = lasth; |
| 1099 | + } else { |
| 1100 | + head = h; |
| 1101 | + } |
1051 | 1102 | break;
|
1052 | 1103 | }
|
1053 | 1104 |
|
1054 | 1105 | if (next.time > limit) {
|
1055 |
| - head = h; |
| 1106 | + if (h.value != null) { |
| 1107 | + TimedNode<Object> lasth = new TimedNode<Object>(null, 0L); |
| 1108 | + lasth.lazySet(h.get()); |
| 1109 | + head = lasth; |
| 1110 | + } else { |
| 1111 | + head = h; |
| 1112 | + } |
1056 | 1113 | break;
|
1057 | 1114 | }
|
1058 | 1115 |
|
@@ -1085,6 +1142,20 @@ public void addFinal(Object notificationLite) {
|
1085 | 1142 | done = true;
|
1086 | 1143 | }
|
1087 | 1144 |
|
| 1145 | + /** |
| 1146 | + * Replace a non-empty head node with an empty one to |
| 1147 | + * allow the GC of the inaccessible old value. |
| 1148 | + */ |
| 1149 | + @Override |
| 1150 | + public void trimHead() { |
| 1151 | + TimedNode<Object> h = head; |
| 1152 | + if (h.value != null) { |
| 1153 | + TimedNode<Object> n = new TimedNode<Object>(null, 0); |
| 1154 | + n.lazySet(h.get()); |
| 1155 | + head = n; |
| 1156 | + } |
| 1157 | + } |
| 1158 | + |
1088 | 1159 | @Override
|
1089 | 1160 | @Nullable
|
1090 | 1161 | @SuppressWarnings("unchecked")
|
|
0 commit comments