@@ -33,106 +33,111 @@ Concurrency
33
33
=====================================
34
34
35
35
* Problem Statement:
36
+ --------------------
37
+
36
38
Implement a thread-safe bounded blocking queue using Java that supports the operations `enqueue` and `dequeue`.
37
39
The queue should block the enqueuing thread if the queue is full and block the dequeuing thread if the queue is empty.
38
40
39
41
* Pseudocode:
42
+ -------------
43
+
44
+ class BoundedBlockingQueue {
45
+ private Queue<Integer> queue;
46
+ private int capacity;
47
+ private Lock lock;
48
+ private Condition notFull;
49
+ private Condition notEmpty;
50
+
51
+ BoundedBlockingQueue(int capacity) {
52
+ this.queue = new LinkedList<>();
53
+ this.capacity = capacity;
54
+ this.lock = new ReentrantLock();
55
+ this.notFull = lock.newCondition();
56
+ this.notEmpty = lock.newCondition();
57
+ }
40
58
41
- class BoundedBlockingQueue {
42
- private Queue<Integer> queue;
43
- private int capacity;
44
- private Lock lock;
45
- private Condition notFull;
46
- private Condition notEmpty;
47
-
48
- BoundedBlockingQueue(int capacity) {
49
- this.queue = new LinkedList<>();
50
- this.capacity = capacity;
51
- this.lock = new ReentrantLock();
52
- this.notFull = lock.newCondition();
53
- this.notEmpty = lock.newCondition();
54
- }
55
-
56
- void enqueue(int item) {
57
- lock.lock();
58
- try {
59
- while (queue.size() == capacity) {
60
- notFull.await();
59
+ void enqueue(int item) {
60
+ lock.lock();
61
+ try {
62
+ while (queue.size() == capacity) {
63
+ notFull.await();
64
+ }
65
+ queue.add(item);
66
+ notEmpty.signalAll();
67
+ } finally {
68
+ lock.unlock();
61
69
}
62
- queue.add(item);
63
- notEmpty.signalAll();
64
- } finally {
65
- lock.unlock();
66
70
}
67
- }
68
71
69
- int dequeue() {
70
- lock.lock();
71
- try {
72
- while (queue.isEmpty()) {
73
- notEmpty.await();
72
+ int dequeue() {
73
+ lock.lock();
74
+ try {
75
+ while (queue.isEmpty()) {
76
+ notEmpty.await();
77
+ }
78
+ int item = queue.remove();
79
+ notFull.signalAll();
80
+ return item;
81
+ } finally {
82
+ lock.unlock();
74
83
}
75
- int item = queue.remove();
76
- notFull.signalAll();
77
- return item;
78
- } finally {
79
- lock.unlock();
80
84
}
81
85
}
82
- }
83
86
84
87
* Java Code:
88
+ ------------
89
+
90
+ import java.util.LinkedList;
91
+ import java.util.Queue;
92
+ import java.util.concurrent.locks.Condition;
93
+ import java.util.concurrent.locks.Lock;
94
+ import java.util.concurrent.locks.ReentrantLock;
95
+
96
+ public class BoundedBlockingQueue {
97
+ private Queue<Integer> queue;
98
+ private int capacity;
99
+ private Lock lock;
100
+ private Condition notFull;
101
+ private Condition notEmpty;
102
+
103
+ public BoundedBlockingQueue(int capacity) {
104
+ this.queue = new LinkedList<>();
105
+ this.capacity = capacity;
106
+ this.lock = new ReentrantLock();
107
+ this.notFull = lock.newCondition();
108
+ this.notEmpty = lock.newCondition();
109
+ }
85
110
86
- import java.util.LinkedList;
87
- import java.util.Queue;
88
- import java.util.concurrent.locks.Condition;
89
- import java.util.concurrent.locks.Lock;
90
- import java.util.concurrent.locks.ReentrantLock;
91
-
92
- public class BoundedBlockingQueue {
93
- private Queue<Integer> queue;
94
- private int capacity;
95
- private Lock lock;
96
- private Condition notFull;
97
- private Condition notEmpty;
98
-
99
- public BoundedBlockingQueue(int capacity) {
100
- this.queue = new LinkedList<>();
101
- this.capacity = capacity;
102
- this.lock = new ReentrantLock();
103
- this.notFull = lock.newCondition();
104
- this.notEmpty = lock.newCondition();
105
- }
106
-
107
- public void enqueue(int item) throws InterruptedException {
108
- lock.lock();
109
- try {
110
- while (queue.size() == capacity) {
111
- notFull.await();
111
+ public void enqueue(int item) throws InterruptedException {
112
+ lock.lock();
113
+ try {
114
+ while (queue.size() == capacity) {
115
+ notFull.await();
116
+ }
117
+ queue.add(item);
118
+ notEmpty.signalAll();
119
+ } finally {
120
+ lock.unlock();
112
121
}
113
- queue.add(item);
114
- notEmpty.signalAll();
115
- } finally {
116
- lock.unlock();
117
122
}
118
- }
119
123
120
- public int dequeue() throws InterruptedException {
121
- lock.lock();
122
- try {
123
- while (queue.isEmpty()) {
124
- notEmpty.await();
124
+ public int dequeue() throws InterruptedException {
125
+ lock.lock();
126
+ try {
127
+ while (queue.isEmpty()) {
128
+ notEmpty.await();
129
+ }
130
+ int item = queue.remove();
131
+ notFull.signalAll();
132
+ return item;
133
+ } finally {
134
+ lock.unlock();
125
135
}
126
- int item = queue.remove();
127
- notFull.signalAll();
128
- return item;
129
- } finally {
130
- lock.unlock();
131
136
}
132
137
}
133
- }
134
138
135
139
* Time and Space Complexity:
140
+ ----------------------------
136
141
137
142
Time Complexity:
138
143
- `enqueue()`: O(1) - Adding an element to the queue is a constant-time operation.
@@ -142,7 +147,8 @@ Concurrency
142
147
- The space complexity is O(n) where n is the capacity of the queue. The queue stores up to `n` elements, and
143
148
additional space is used for locks and condition variables.
144
149
145
- * Considerations for Concurrency in Coding Interviews
150
+ * Considerations for Concurrency in Coding Interviews:
151
+ -------------------------------------------------------
146
152
147
153
1. Thread Safety: Ensure your code handles concurrent access correctly, avoiding race conditions and deadlocks.
148
154
2. Synchronization: Use appropriate synchronization primitives to manage access to shared resources. In Java, this
@@ -158,86 +164,94 @@ Concurrency
158
164
The Dining Philosophers problem is a classic example of synchronization issues in concurrent programming. To solve this problem,
159
165
we need to ensure that no philosopher starves and that the system avoids deadlocks (ChatGPT coded the solution 🤖).
160
166
161
- * Solution Explanation
167
+ * Solution Explanation:
168
+ -----------------------
162
169
163
170
One common approach to solve this problem is to enforce an ordering on the acquisition of forks to prevent circular wait conditions
164
171
that could lead to a deadlock. Here, we can impose an order by making each philosopher pick up the lower-numbered fork first, and
165
172
then the higher-numbered fork. This ensures that there is no circular wait.
166
173
167
- * Java Implementation
174
+ * Java Implementation:
175
+ ----------------------
168
176
169
177
Here's the Java implementation of the Dining Philosophers problem using the above strategy:
170
178
171
- import java.util.concurrent.locks.Lock;
172
- import java.util.concurrent.locks.ReentrantLock;
179
+ import java.util.concurrent.locks.Lock;
180
+ import java.util.concurrent.locks.ReentrantLock;
173
181
174
- public class DiningPhilosophers {
182
+ public class DiningPhilosophers {
175
183
176
- private final Lock[] forks = new ReentrantLock[5];
184
+ private final Lock[] forks = new ReentrantLock[5];
177
185
178
- public DiningPhilosophers() {
179
- for (int i = 0; i < 5; i++) {
180
- forks[i] = new ReentrantLock();
186
+ public DiningPhilosophers() {
187
+ for (int i = 0; i < 5; i++) {
188
+ forks[i] = new ReentrantLock();
189
+ }
181
190
}
182
- }
183
191
184
- public void wantsToEat(int philosopher,
185
- Runnable pickLeftFork,
186
- Runnable pickRightFork,
187
- Runnable eat,
188
- Runnable putLeftFork,
189
- Runnable putRightFork) throws InterruptedException {
190
- int leftFork = philosopher;
191
- int rightFork = (philosopher + 1) % 5;
192
-
193
- // Pick forks in a globally consistent order to avoid deadlocks
194
- if (philosopher % 2 == 0) {
195
- forks[leftFork].lock();
196
- pickLeftFork.run();
197
- forks[rightFork].lock();
198
- pickRightFork.run();
199
- } else {
200
- forks[rightFork].lock();
201
- pickRightFork.run();
202
- forks[leftFork].lock();
203
- pickLeftFork.run();
204
- }
192
+ public void wantsToEat(int philosopher,
193
+ Runnable pickLeftFork,
194
+ Runnable pickRightFork,
195
+ Runnable eat,
196
+ Runnable putLeftFork,
197
+ Runnable putRightFork) throws InterruptedException {
198
+ int leftFork = philosopher;
199
+ int rightFork = (philosopher + 1) % 5;
200
+
201
+ // Pick forks in a globally consistent order to avoid deadlocks
202
+ if (philosopher % 2 == 0) {
203
+ forks[leftFork].lock();
204
+ pickLeftFork.run();
205
+ forks[rightFork].lock();
206
+ pickRightFork.run();
207
+ } else {
208
+ forks[rightFork].lock();
209
+ pickRightFork.run();
210
+ forks[leftFork].lock();
211
+ pickLeftFork.run();
212
+ }
205
213
206
- // Eat
207
- eat.run();
208
-
209
- // Put down forks in the reverse order of picking up
210
- if (philosopher % 2 == 0) {
211
- putRightFork.run();
212
- forks[rightFork].unlock();
213
- putLeftFork.run();
214
- forks[leftFork].unlock();
215
- } else {
216
- putLeftFork.run();
217
- forks[leftFork].unlock();
218
- putRightFork.run();
219
- forks[rightFork].unlock();
214
+ // Eat
215
+ eat.run();
216
+
217
+ // Put down forks in the reverse order of picking up
218
+ if (philosopher % 2 == 0) {
219
+ putRightFork.run();
220
+ forks[rightFork].unlock();
221
+ putLeftFork.run();
222
+ forks[leftFork].unlock();
223
+ } else {
224
+ putLeftFork.run();
225
+ forks[leftFork].unlock();
226
+ putRightFork.run();
227
+ forks[rightFork].unlock();
228
+ }
220
229
}
221
230
}
222
- }
223
- ```
224
231
225
- * Explanation
232
+ * Explanation:
233
+ --------------
226
234
227
- 1. Lock Array: We use an array of `ReentrantLock` objects to represent the forks. Each fork is shared between two adjacent philosophers.
228
- 2. Consistent Order: Philosophers pick up forks in a consistent order based on their ID to avoid deadlock:
229
- - Even-numbered philosophers pick up the left fork first and then the right fork.
230
- - Odd-numbered philosophers pick up the right fork first and then the left fork.
231
- 3. Locking: The `lock` method ensures that a philosopher can only proceed if both forks are available. This prevents any race conditions.
232
- 4. Unlocking: After eating, philosophers put down the forks in the reverse order they were picked up to maintain consistency and release
233
- the resources properly.
235
+ 1. Lock Array: We use an array of `ReentrantLock` objects to represent the forks. Each fork is shared between
236
+ two adjacent philosophers.
237
+ 2. Consistent Order: Philosophers pick up forks in a consistent order based on their ID to avoid deadlock:
238
+ - Even-numbered philosophers pick up the left fork first and then the right fork.
239
+ - Odd-numbered philosophers pick up the right fork first and then the left fork.
240
+ 3. Locking: The `lock` method ensures that a philosopher can only proceed if both forks are available. This
241
+ prevents any race conditions.
242
+ 4. Unlocking: After eating, philosophers put down the forks in the reverse order they were picked up to maintain
243
+ consistency and release the resources properly.
234
244
235
- * Time and Space Complexity
245
+ * Time and Space Complexity:
246
+ ----------------------------
236
247
237
- Time Complexity:
238
- - The time complexity for each philosopher to pick up and put down the forks is O(1) because locking and unlocking are constant-time operations.
248
+ Time Complexity:
249
+ - The time complexity for each philosopher to pick up and put down the forks is O(1) because locking and unlocking
250
+ are constant-time operations.
239
251
240
- Space Complexity:
241
- - The space complexity is O(1) for each philosopher since we only store references to the forks and use a constant amount of extra space (five locks).
252
+ Space Complexity:
253
+ - The space complexity is O(1) for each philosopher since we only store references to the forks and use a constant
254
+ amount of extra space (five locks).
242
255
243
- This solution ensures that no philosopher will starve and that the system avoids deadlocks, fulfilling the requirements of the problem.
256
+ This solution ensures that no philosopher will starve and that the system avoids deadlocks, fulfilling the requirements
257
+ of the problem.
0 commit comments