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    *     http://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     public 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     public 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     public 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 }