View Javadoc
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 }