锁面试题

​ 题:创建一个固定容量的同步容器,拥有put和get以及getCount方法,支持2个生产者线程和10个消费者线程阻塞调用

使用synchronized

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public class Question01<T> {

private final int MAX = 10;
private LinkedList<T> list = new LinkedList<>();
private int count = 0;

public synchronized void put(T t) {
while (list.size() == MAX) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.add(t);
++count;
this.notifyAll();
}

public synchronized T get() {
while (0 == list.size()) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
T t = list.removeFirst();
--count;
this.notifyAll();
return t;
}

public synchronized int getCount() {
return count;
}

public static void main(String[] args) {
Question01<String> question01 = new Question01<>();

for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 5; j++) {
System.out.println(question01.get());
}
}, "c" + i).start();
}

try {
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}

for (int i = 0; i < 2; i++) {
new Thread(() -> {
for (int j = 0; j < 25; j++) {
question01.put(Thread.currentThread().getName() + " " + j);
}
}, "p" + i).start();
}

}

}

​ 这里有两个需要注意的地方,一个是判断的时候是使用的while循环判断,另一个唤醒线程使用的是notifyAll而不是notify。

​ 原因:1. 这里之所以不能使用 if 判断的原因是,如果有多个生产者线程同时进入等待状态,此时消费者线程消费了数据,使用notifyAll唤醒所有的生产者线程,从等待的地方开始继续往下执行。假设此时 list 的size已经等于MAX - 1了,但是仍然有两个或者多个生产者线程处于等待状态,那么当其中的任何一个获得锁执行完之后,此时的list的长度已经达到最大值,那么当其他的线程获得锁,继续往下执行的时候,如果此时的判断是if (list.size() == MAX) 那么线程不会再次判断,而是直接执行list.add(t); 往list中添加数据,那么此时肯定会超过最大值了。而如果使用的是while的话,那么从wait的地方继续执行后面的代码的时候,还会再次进行判断,这样就不会超出最大容量了。所以,当使用wait的使用,99.9%的情况都是与while一同使用的。

​ 原因:2. 如果使用的是notify的话,假如现在是生产者调用了notify,那么它不能保证它唤醒的一定是消费者线程,也有可能是生产者线程,如果唤醒的是生产者线程,那么又会进入到wait状态。

使用ReentrantLock的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
public class Question02<T> {

private final int MAX = 10;
private LinkedList<T> list = new LinkedList<>();
private int count = 0;

private Lock lock = new ReentrantLock();
private Condition producer = lock.newCondition();
private Condition consumer = lock.newCondition();

public void put(T t) {
try {
lock.lock();
while (list.size() == MAX) {
producer.await();
}
list.add(t);
count++;
consumer.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}

public synchronized T get() {
T t = null;
try {
lock.lock();
while (list.size() == 0) {
consumer.await();
}
t = list.removeFirst();
count--;
producer.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
return t;
}

public synchronized int getCount() {
return count;
}

public static void main(String[] args) {
Question02<String> question01 = new Question02<>();

for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 5; j++) {
System.out.println(question01.get());
}
}, "c" + i).start();
}

try {
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}

for (int i = 0; i < 2; i++) {
new Thread(() -> {
for (int j = 0; j < 25; j++) {
question01.put(Thread.currentThread().getName() + " " + j);
}
}, "p" + i).start();
}

}

}

​ 使用ReentrantLock的方式,可以精确指定哪些线程被唤醒,这样的效率明显是比synchronized效率要高的。