1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 package org.apache.commons.configuration2.sync;
18
19 import static org.junit.jupiter.api.Assertions.assertEquals;
20 import static org.mockito.Mockito.mock;
21 import static org.mockito.Mockito.verify;
22 import static org.mockito.Mockito.verifyNoMoreInteractions;
23 import static org.mockito.Mockito.when;
24
25 import java.util.Random;
26 import java.util.concurrent.locks.Lock;
27 import java.util.concurrent.locks.ReadWriteLock;
28
29 import org.junit.jupiter.api.Test;
30
31 /**
32 * Test class for {@code ReadWriteSynchronizer}.
33 */
34 public class TestReadWriteSynchronizer {
35 /**
36 * A class representing an account.
37 */
38 private static final class Account {
39 /** The amount stored in this account. */
40 private long amount;
41
42 /**
43 * Changes the amount of money by the given delata.
44 *
45 * @param delta the delta
46 */
47 public void change(final long delta) {
48 amount += delta;
49 }
50
51 /**
52 * Returns the amount of money stored in this account.
53 *
54 * @return the amount
55 */
56 public long getAmount() {
57 return amount;
58 }
59 }
60
61 /**
62 * A thread which performs a number of read operations on the bank's accounts and checks whether the amount of money is
63 * consistent.
64 */
65 private static final class ReaderThread extends Thread {
66 /** The acounts to monitor. */
67 private final Account[] accounts;
68
69 /** The synchronizer object. */
70 private final Synchronizer sync;
71
72 /** The number of read operations. */
73 private final int numberOfReads;
74
75 /** Stores errors detected on read operations. */
76 private volatile int errors;
77
78 /**
79 * Creates a new instance of {@code ReaderThread}.
80 *
81 * @param s the synchronizer to be used
82 * @param readCount the number of read operations
83 * @param accs the accounts to monitor
84 */
85 public ReaderThread(final Synchronizer s, final int readCount, final Account... accs) {
86 accounts = accs;
87 sync = s;
88 numberOfReads = readCount;
89 }
90
91 /**
92 * Returns the number of errors occurred during read operations.
93 *
94 * @return the number of errors
95 */
96 public int getErrors() {
97 return errors;
98 }
99
100 /**
101 * Performs the given number of read operations.
102 */
103 @Override
104 public void run() {
105 for (int i = 0; i < numberOfReads; i++) {
106 sync.beginRead();
107 final long sum = sumUpAccounts(accounts);
108 sync.endRead();
109 if (sum != TOTAL_MONEY) {
110 errors++;
111 }
112 }
113 }
114 }
115
116 /**
117 * A test thread for updating account objects. This thread executes a number of transactions on two accounts. Each
118 * transaction determines the account containing more money. Then a random number of money is transferred from this
119 * account to the other one.
120 */
121 private static final class UpdateThread extends Thread {
122 /** The synchronizer. */
123 private final Synchronizer sync;
124
125 /** Account 1. */
126 private final Account account1;
127
128 /** Account 2. */
129 private final Account account2;
130
131 /** An object for creating random numbers. */
132 private final Random random;
133
134 /** The number of transactions. */
135 private final int numberOfUpdates;
136
137 /**
138 * Creates a new instance of {@code UpdateThread}.
139 *
140 * @param s the synchronizer
141 * @param updateCount the number of updates
142 * @param ac1 account 1
143 * @param ac2 account 2
144 */
145 public UpdateThread(final Synchronizer s, final int updateCount, final Account ac1, final Account ac2) {
146 sync = s;
147 account1 = ac1;
148 account2 = ac2;
149 numberOfUpdates = updateCount;
150 random = new Random();
151 }
152
153 /**
154 * Performs the given number of update transactions.
155 */
156 @Override
157 public void run() {
158 for (int i = 0; i < numberOfUpdates; i++) {
159 sync.beginWrite();
160 final Account acSource;
161 Account acDest;
162 if (account1.getAmount() < account2.getAmount()) {
163 acSource = account1;
164 acDest = account2;
165 } else {
166 acSource = account2;
167 acDest = account1;
168 }
169 final long x = Math.round(random.nextDouble() * (acSource.getAmount() - 1)) + 1;
170 acSource.change(-x);
171 acDest.change(x);
172 sync.endWrite();
173 }
174 }
175 }
176
177 /** Constant for the total amount of money in the system. */
178 private static final long TOTAL_MONEY = 1000000L;
179
180 /**
181 * Helper method to calculate the sum over all accounts.
182 *
183 * @param accounts the accounts to check
184 * @return the sum of the money on these accounts
185 */
186 private static long sumUpAccounts(final Account... accounts) {
187 long sum = 0;
188 for (final Account acc : accounts) {
189 sum += acc.getAmount();
190 }
191 return sum;
192 }
193
194 /**
195 * Tests whether a lock passed to the constructor is used.
196 */
197 @Test
198 void testInitLock() {
199 final ReadWriteLock lock = mock(ReadWriteLock.class);
200 final Lock readLock = mock(Lock.class);
201
202 when(lock.readLock()).thenReturn(readLock);
203
204 final ReadWriteSynchronizer sync = new ReadWriteSynchronizer(lock);
205 sync.beginRead();
206
207 verify(lock).readLock();
208 verify(readLock).lock();
209 verifyNoMoreInteractions(lock, readLock);
210 }
211
212 /**
213 * Tests whether the synchronizer is reentrant. This is important for some combined operations on a configuration.
214 */
215 @Test
216 void testReentrance() {
217 final Synchronizer sync = new ReadWriteSynchronizer();
218 sync.beginWrite();
219 sync.beginRead();
220 sync.beginRead();
221 sync.endRead();
222 sync.endRead();
223 sync.beginWrite();
224 sync.endWrite();
225 sync.endWrite();
226 }
227
228 /**
229 * Performs a test of the synchronizer based on the classic example of account objects. Money is transferred between two
230 * accounts. If everything goes well, the total amount of money stays constant over time.
231 */
232 @Test
233 void testSynchronizerInAction() throws InterruptedException {
234 final int numberOfUpdates = 10000;
235 final int numberOfReads = numberOfUpdates / 2;
236 final int readThreadCount = 3;
237 final int updateThreadCount = 2;
238
239 final Synchronizer sync = new ReadWriteSynchronizer();
240 final Account account1 = new Account();
241 final Account account2 = new Account();
242 account1.change(TOTAL_MONEY / 2);
243 account2.change(TOTAL_MONEY / 2);
244
245 final UpdateThread[] updateThreads = new UpdateThread[updateThreadCount];
246 for (int i = 0; i < updateThreads.length; i++) {
247 updateThreads[i] = new UpdateThread(sync, numberOfUpdates, account1, account2);
248 updateThreads[i].start();
249 }
250 final ReaderThread[] readerThreads = new ReaderThread[readThreadCount];
251 for (int i = 0; i < readerThreads.length; i++) {
252 readerThreads[i] = new ReaderThread(sync, numberOfReads, account1, account2);
253 readerThreads[i].start();
254 }
255
256 for (final UpdateThread t : updateThreads) {
257 t.join();
258 }
259 for (final ReaderThread t : readerThreads) {
260 t.join();
261 assertEquals(0, t.getErrors());
262 }
263 sync.beginRead();
264 assertEquals(TOTAL_MONEY, sumUpAccounts(account1, account2));
265 sync.endRead();
266 }
267 }