|
1 | 1 | package io.sentry.util; |
2 | 2 |
|
3 | 3 | import io.sentry.ISentryLifecycleToken; |
| 4 | +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; |
4 | 5 | import java.util.concurrent.locks.ReentrantLock; |
| 6 | +import org.jetbrains.annotations.ApiStatus; |
5 | 7 | import org.jetbrains.annotations.NotNull; |
| 8 | +import org.jetbrains.annotations.Nullable; |
| 9 | +import org.jetbrains.annotations.TestOnly; |
6 | 10 |
|
7 | | -public final class AutoClosableReentrantLock extends ReentrantLock { |
| 11 | +/** |
| 12 | + * Hands out an {@link ISentryLifecycleToken} from {@link #acquire()} for use with |
| 13 | + * try-with-resources (replacing {@code synchronized} blocks). |
| 14 | + * |
| 15 | + * <p>The underlying {@link ReentrantLock} is created lazily on the first {@link #acquire()}. Many |
| 16 | + * SDK objects hold a lock but never contend on it (especially during {@code SentryAndroid.init}), |
| 17 | + * so the eager allocation of a {@link ReentrantLock} (and its {@code AbstractQueuedSynchronizer}) |
| 18 | + * was pure GC and main-thread overhead. We keep a {@link ReentrantLock} rather than reverting to |
| 19 | + * {@code synchronized} to stay friendly to virtual threads (Loom), see #3715. |
| 20 | + * |
| 21 | + * <p>{@link #acquire()} returns this instance as the token, so the steady-state acquire/close path |
| 22 | + * allocates nothing. Reentrant acquires stay balanced because try-with-resources calls {@link |
| 23 | + * #close()} exactly once per acquire. |
| 24 | + */ |
| 25 | +@ApiStatus.Internal |
| 26 | +public final class AutoClosableReentrantLock implements ISentryLifecycleToken { |
8 | 27 |
|
9 | | - private static final long serialVersionUID = -3283069816958445549L; |
| 28 | + private static final @NotNull AtomicReferenceFieldUpdater< |
| 29 | + AutoClosableReentrantLock, ReentrantLock> |
| 30 | + LOCK_UPDATER = |
| 31 | + AtomicReferenceFieldUpdater.newUpdater( |
| 32 | + AutoClosableReentrantLock.class, ReentrantLock.class, "lock"); |
10 | 33 |
|
11 | | - public ISentryLifecycleToken acquire() { |
12 | | - lock(); |
13 | | - return new AutoClosableReentrantLockLifecycleToken(this); |
14 | | - } |
| 34 | + private volatile @Nullable ReentrantLock lock; |
15 | 35 |
|
16 | | - static final class AutoClosableReentrantLockLifecycleToken implements ISentryLifecycleToken { |
| 36 | + public @NotNull ISentryLifecycleToken acquire() { |
| 37 | + getOrCreateLock().lock(); |
| 38 | + return this; |
| 39 | + } |
17 | 40 |
|
18 | | - private final @NotNull ReentrantLock lock; |
| 41 | + @Override |
| 42 | + public void close() { |
| 43 | + Objects.requireNonNull(lock, "close() called before acquire()").unlock(); |
| 44 | + } |
19 | 45 |
|
20 | | - AutoClosableReentrantLockLifecycleToken(final @NotNull ReentrantLock lock) { |
21 | | - this.lock = lock; |
| 46 | + private @NotNull ReentrantLock getOrCreateLock() { |
| 47 | + final @Nullable ReentrantLock existing = lock; |
| 48 | + if (existing != null) { |
| 49 | + return existing; |
22 | 50 | } |
23 | | - |
24 | | - @Override |
25 | | - public void close() { |
26 | | - lock.unlock(); |
| 51 | + final @NotNull ReentrantLock candidate = new ReentrantLock(); |
| 52 | + if (LOCK_UPDATER.compareAndSet(this, null, candidate)) { |
| 53 | + return candidate; |
27 | 54 | } |
| 55 | + // The CAS can only fail because another thread installed its lock first, and the field is |
| 56 | + // never reset, so all callers end up contending on that same instance. |
| 57 | + return Objects.requireNonNull(lock, "lock must have been set by the winning thread"); |
| 58 | + } |
| 59 | + |
| 60 | + @TestOnly |
| 61 | + boolean isLocked() { |
| 62 | + final @Nullable ReentrantLock current = lock; |
| 63 | + return current != null && current.isLocked(); |
| 64 | + } |
| 65 | + |
| 66 | + @TestOnly |
| 67 | + boolean isLockAllocated() { |
| 68 | + return lock != null; |
28 | 69 | } |
29 | 70 | } |
0 commit comments