- 经典面试题
ー个容器,提供两个方法,add,size写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个时,线程2给出提示并结束,如何实现?
方法一:使用volatile
使用 volatile关键字,volatile保证了线程间的可见性,但是其并不保证原子性。
在这简单介绍一下volatile
- 对于volatile修饰的变量,jvm虚拟机只是保证从主内存加载到线程工作内存的值是最新的。可以理解为当线程1改变了缓存中值,则会提醒线程2在read时,先去主存中重新load一下,确保每次加载到的都是最新的值。
- 在.class文件中,volatile关键字所修饰的变量,写操作时都会加上lock信号量。这样做的目的是:
(1)保证在该信号生效期间会独占共享内存,并将处理器缓存行的数据写回到内存中。
(2)当缓存行的数据写回到内存的操作会使得其他cpu缓存了该地址的数据变为无效,必须重新去主存中重洗获取。
具体分析
看了上述的原理分析,我们用volatile修饰存储元素的容器(数组、集合)。从而保证了当一个线程改变了容器的值,
都会提醒其他线程去重新加载这个容器,进而达到了信息同步的效果。
代码实现
public class Test01 {
// volatile修饰List集合
volatile List<Object> lists = new ArrayList<>();
void adds(Object object) {
lists.add(object);
}
int size() {
return lists.size();
}
public static void main(String[] args) {
Test01 t = new Test01();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
t.adds(new Object());
System.out.println(t.size());
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
while (true) {
if (t.size() == 5) {
System.out.println("daole");
break;
}
}
}).start();
}
}
运行结果截图:
方法二:使用wait 、notify
简单介绍
想必大家对wait 、notify、notifyAll都很熟悉了,他们都是object类下的方法,使用他们时必须把他们放在synchronized代码块中。
并且需要注意的是wait()会释放当前所占的锁,notify()和 notifyAll()不会释放锁
具体分析
- 在这里先启动监听的线程,再启动添加元素的线程。先让监听线程进入wait状态,释放所占有的锁,然后添加元素线程启动获取锁,当添加元素的个数到达5时,唤醒监听线程,并进入wait状态(释放锁),等待监听线程唤醒。
- 在唤醒监听线程的时候,当监听线程打印提示信息后,需要重新进入wait状态,原因是:因为synchronized锁的是对象,此时监听线程获取了锁,当执行打印完提示信息后,需要释放锁资源,以便添加元素的线程重新获得锁,并且继续添加剩余的元素。
代码实现
public class Test03 {
// volatile List<Object> lists = new ArrayList<>();
int count = 0;
void adds() {
count++;
}
int size() {
return count;
}
public static void main(String[] args) {
Test03 t = new Test03();
new Thread(() -> {
synchronized (t) {
try {
// 监听线程启动,并且此时进入了wait状态,并且释放了锁资源
t.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("daole");
// 打印完提示信息,并唤醒添加元素的线程
t.notify();
try {
// 释放锁,因为监听线程的目的已经实现,为了节约资源让其等待1s后关闭
t.wait(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
synchronized (t) {
for (int i = 1; i <= 10; i++) {
t.adds();
// 判断集合中的元素个数,当到达5时 唤醒监听线程并且进入wait状态并且释放锁。
if (t.size() == 5) {
t.notify();
try {
t.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(t.size());
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
运行结果截图
方法三:使用门闩 CountDownLatch 替代 wait notify(推荐)
简单介绍
CountDownLatch 不涉及锁定,利用它可以实现类似计数器的功能,每次调用countDown()方法计数器减 1,
当count的值为0时当前线程继续执行
具体分析
- 先创建一个CountDownLatch类对象,并且使count值等于1.
- 先启动监听线程,让其进入await状态(await和wait的功能一致,只是属于不同的package下)
- 再启动添加元素线程,当长度到达5时,调用countDown()方法,继续执行监听线程的run()方法,打印提示信息。
代码实现
public class Test04 {
// volatile List<Object> lists = new ArrayList<>();
int count = 0;
void adds() {
count++;
}
int size() {
return count;
}
public static void main(String[] args) {
Test04 t = new Test04();
CountDownLatch countDownLatch = new CountDownLatch(1);
new Thread(() -> {
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("daole");
}).start();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
t.adds();
if (t.size() == 5) {
countDownLatch.countDown();
}
System.out.println(t.size());
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
运行截图