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 /**
37 * A class representing an account.
38 */
39 private static final class Account {
40
41 /** The amount stored in this account. */
42 private long amount;
43
44 /**
45 * Changes the amount of money by the given delata.
46 *
47 * @param delta the delta
48 */
49 public void change(final long delta) {
50 amount += delta;
51 }
52
53 /**
54 * Returns the amount of money stored in this account.
55 *
56 * @return the amount
57 */
58 public long getAmount() {
59 return amount;
60 }
61 }
62
63 /**
64 * A thread which performs a number of read operations on the bank's accounts and checks whether the amount of money is
65 * consistent.
66 */
67 private static final class ReaderThread extends Thread {
68
69 /** The acounts to monitor. */
70 private final Account[] accounts;
71
72 /** The synchronizer object. */
73 private final Synchronizer sync;
74
75 /** The number of read operations. */
76 private final int numberOfReads;
77
78 /** Stores errors detected on read operations. */
79 private volatile int errors;
80
81 /**
82 * Creates a new instance of {@code ReaderThread}.
83 *
84 * @param s the synchronizer to be used
85 * @param readCount the number of read operations
86 * @param accs the accounts to monitor
87 */
88 public ReaderThread(final Synchronizer s, final int readCount, final Account... accs) {
89 accounts = accs;
90 sync = s;
91 numberOfReads = readCount;
92 }
93
94 /**
95 * Returns the number of errors occurred during read operations.
96 *
97 * @return the number of errors
98 */
99 public int getErrors() {
100 return errors;
101 }
102
103 /**
104 * Performs the given number of read operations.
105 */
106 @Override
107 public void run() {
108 for (int i = 0; i < numberOfReads; i++) {
109 sync.beginRead();
110 final long sum = sumUpAccounts(accounts);
111 sync.endRead();
112 if (sum != TOTAL_MONEY) {
113 errors++;
114 }
115 }
116 }
117 }
118
119 /**
120 * A test thread for updating account objects. This thread executes a number of transactions on two accounts. Each
121 * transaction determines the account containing more money. Then a random number of money is transferred from this
122 * account to the other one.
123 */
124 private static final class UpdateThread extends Thread {
125
126 /** The synchronizer. */
127 private final Synchronizer sync;
128
129 /** Account 1. */
130 private final Account account1;
131
132 /** Account 2. */
133 private final Account account2;
134
135 /** An object for creating random numbers. */
136 private final Random random;
137
138 /** The number of transactions. */
139 private final int numberOfUpdates;
140
141 /**
142 * Creates a new instance of {@code UpdateThread}.
143 *
144 * @param s the synchronizer
145 * @param updateCount the number of updates
146 * @param ac1 account 1
147 * @param ac2 account 2
148 */
149 public UpdateThread(final Synchronizer s, final int updateCount, final Account ac1, final Account ac2) {
150 sync = s;
151 account1 = ac1;
152 account2 = ac2;
153 numberOfUpdates = updateCount;
154 random = new Random();
155 }
156
157 /**
158 * Performs the given number of update transactions.
159 */
160 @Override
161 public void run() {
162 for (int i = 0; i < numberOfUpdates; i++) {
163 sync.beginWrite();
164 final Account acSource;
165 Account acDest;
166 if (account1.getAmount() < account2.getAmount()) {
167 acSource = account1;
168 acDest = account2;
169 } else {
170 acSource = account2;
171 acDest = account1;
172 }
173 final long x = Math.round(random.nextDouble() * (acSource.getAmount() - 1)) + 1;
174 acSource.change(-x);
175 acDest.change(x);
176 sync.endWrite();
177 }
178 }
179 }
180
181 /** Constant for the total amount of money in the system. */
182 private static final long TOTAL_MONEY = 1000000L;
183
184 /**
185 * Helper method to calculate the sum over all accounts.
186 *
187 * @param accounts the accounts to check
188 * @return the sum of the money on these accounts
189 */
190 private static long sumUpAccounts(final Account... accounts) {
191 long sum = 0;
192 for (final Account acc : accounts) {
193 sum += acc.getAmount();
194 }
195 return sum;
196 }
197
198 /**
199 * Tests whether a lock passed to the constructor is used.
200 */
201 @Test
202 void testInitLock() {
203 final ReadWriteLock lock = mock(ReadWriteLock.class);
204 final Lock readLock = mock(Lock.class);
205
206 when(lock.readLock()).thenReturn(readLock);
207
208 final ReadWriteSynchronizer sync = new ReadWriteSynchronizer(lock);
209 sync.beginRead();
210
211 verify(lock).readLock();
212 verify(readLock).lock();
213 verifyNoMoreInteractions(lock, readLock);
214 }
215
216 /**
217 * Tests whether the synchronizer is reentrant. This is important for some combined operations on a configuration.
218 */
219 @Test
220 void testReentrance() {
221 final Synchronizer sync = new ReadWriteSynchronizer();
222 sync.beginWrite();
223 sync.beginRead();
224 sync.beginRead();
225 sync.endRead();
226 sync.endRead();
227 sync.beginWrite();
228 sync.endWrite();
229 sync.endWrite();
230 }
231
232 /**
233 * Performs a test of the synchronizer based on the classic example of account objects. Money is transferred between two
234 * accounts. If everything goes well, the total amount of money stays constant over time.
235 */
236 @Test
237 void testSynchronizerInAction() throws InterruptedException {
238 final int numberOfUpdates = 10000;
239 final int numberOfReads = numberOfUpdates / 2;
240 final int readThreadCount = 3;
241 final int updateThreadCount = 2;
242
243 final Synchronizer sync = new ReadWriteSynchronizer();
244 final Account account1 = new Account();
245 final Account account2 = new Account();
246 account1.change(TOTAL_MONEY / 2);
247 account2.change(TOTAL_MONEY / 2);
248
249 final UpdateThread[] updateThreads = new UpdateThread[updateThreadCount];
250 for (int i = 0; i < updateThreads.length; i++) {
251 updateThreads[i] = new UpdateThread(sync, numberOfUpdates, account1, account2);
252 updateThreads[i].start();
253 }
254 final ReaderThread[] readerThreads = new ReaderThread[readThreadCount];
255 for (int i = 0; i < readerThreads.length; i++) {
256 readerThreads[i] = new ReaderThread(sync, numberOfReads, account1, account2);
257 readerThreads[i].start();
258 }
259
260 for (final UpdateThread t : updateThreads) {
261 t.join();
262 }
263 for (final ReaderThread t : readerThreads) {
264 t.join();
265 assertEquals(0, t.getErrors());
266 }
267 sync.beginRead();
268 assertEquals(TOTAL_MONEY, sumUpAccounts(account1, account2));
269 sync.endRead();
270 }
271 }