引言
java面试中经常会遇到这个问题,如何用两个线程交替打印奇偶数。线程A打印1,线程B打印2,线程A打印3,线程B打印4...这个问题的解题思路是协调两个线程的执行时机,线程A与线程B需要交替执行。实现的方式很多,常用的方法是采用wait/notify方法。
本文记录了笔者解决这个问题的过程,并解释过程中遇到的坑以及如何填坑的,以供朋友们参考。
一种有问题的实现方式
代码思路是这样的:既然要两个线程交替打印奇偶数,那就让两个线程共享一个count,当数字是奇数是线程A打印,当数字是偶数时线程B打印,执行完打印操作后自增count,并利用wait/notify方法去阻塞和唤醒线程。接下来看代码:
public class WrongCountDemo {
static class PrintOdd implements Runnable {
private Integer count;
public PrintOdd(Integer count) {
this.count = count;
}
@Override
public void run() {
try {
synchronized (count) {
while (count <= 100) {
if (count % 2 == 1) {
count.wait();
} else {
System.out.println("PrintOdd thread print..." + count++);
count.notify();
}
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
static class PrintEven implements Runnable {
private Integer count;
public PrintEven(Integer count) {
this.count = count;
}
@Override
public void run() {
try {
synchronized (count) {
while (count <= 100) {
if (count % 2 == 0) {
count.wait();
} else {
System.out.println("PrintEven thread print..." + count++);
count.notify();
}
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
public static void main(String[] args) {
Integer count = 1;
PrintOdd printOdd = new PrintOdd(count);
PrintEven printEven = new PrintEven(count);
new Thread(printOdd).start();
new Thread(printEven).start();
}
}
这段代码并不能获得期望的结果,执行结果如下:
PrintEven thread print...1
java.lang.IllegalMonitorStateException
at java.lang.Object.notify(Native Method)
at com.young.print.WrongCountDemo$PrintEven.run(WrongCountDemo.java:52)
at java.lang.Thread.run(Thread.java:745)
问题分析
代码只打印了数字1就抛了异常IllegalMonitorStateException。
查看异常栈,这个异常是打印完数字1后调用count.notify()抛出的。
* @throws IllegalMonitorStateException if the current thread is not
* the owner of this object's monitor.
* @see java.lang.Object#notifyAll()
* @see java.lang.Object#wait()
*/
public final native void notify();
查看notify方法的源码会发现,IllegalMonitorStateException异常的抛出机制是当前调用线程不是对象锁的持有者。
这就是问题的根源所在,虽然代码里通过synchronized关键字锁住了count对象,但由于count对象执行完自增操作后对象发生变化,所以执行count.notify()时的cout对象已经不是线程synchronized锁住的那个count对象。
那如何修改代码呢?答案是重新定义一个wrapper对象包住Count对象,count属性自增而wrapper对象始终不变。
正确的实现方式
public class CountDemo {
static class Wrapper {
private Integer count;
public Wrapper(Integer count) {
this.count = count;
}
public Integer getCount() {
return count;
}
public void setCount(Integer count) {
this.count = count;
}
}
static class PrintOdd implements Runnable {
private Wrapper wrapper;
public PrintOdd(Wrapper wrapper) {
this.wrapper = wrapper;
}
@Override
public void run() {
try {
synchronized (wrapper) {
while (wrapper.getCount() <= 100) {
if (wrapper.getCount() % 2 == 0) {
wrapper.wait();
} else {
System.out.println("PrintOdd thread print..." + wrapper.getCount());
wrapper.setCount(wrapper.getCount() + 1);
wrapper.notify();
}
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
static class PrintEven implements Runnable {
private Wrapper wrapper;
public PrintEven(Wrapper wrapper) {
this.wrapper = wrapper;
}
@Override
public void run() {
try {
synchronized (wrapper) {
while (wrapper.getCount() <= 100) {
if (wrapper.getCount() % 2 == 1) {
wrapper.wait();
} else {
System.out.println("PrintEven thread print..." + wrapper.getCount());
wrapper.setCount(wrapper.getCount() + 1);
wrapper.notify();
}
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
public static void main(String[] args) {
Wrapper wrapper = new Wrapper(1);
PrintOdd printOdd = new PrintOdd(wrapper);
PrintEven printEven = new PrintEven(wrapper);
new Thread(printOdd).start();
new Thread(printEven).start();
}
}
代码执行结果如下:
PrintOdd thread print...1
PrintEven thread print...2
PrintOdd thread print...3
PrintEven thread print...4
...(省略展示)...
PrintOdd thread print...99
PrintEven thread print...100
wait/notify正确的使用姿势
synchronized (sharedObject) {
while (condition) {
// 条件不满足,当前线程阻塞等待
sharedObject.wait();
}
// 执行条件满足时的逻辑
doSomething();
// 通知阻塞的线程可以唤醒
sharedObject.notify();
}
利用ReentrantLock锁实现
众所周知,除了synchronized关键字的加锁方式,还可以使用Lock锁实现对共享变量的同步控制。利用Lock对象还可以生成Condition条件,Condition条件中包含了await/signal方法,可以用来替代Object对象的wait/notify方法。
下面的代码演示了利用ReentrantLock可重入锁实现两个线程交替打印奇偶数的实现,逻辑与前面的代码相似。
public class CountLockDemo {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
Wrapper wrapper = new Wrapper(1);
new Thread(new PrintOdd(lock, condition, wrapper)).start();
new Thread(new PrintEven(lock, condition, wrapper)).start();
}
static class Wrapper {
private Integer count;
public Wrapper(Integer count) {
this.count = count;
}
public Integer getCount() {
return count;
}
public void setCount(Integer count) {
this.count = count;
}
}
static class PrintOdd implements Runnable {
private volatile Wrapper wrapper;
private ReentrantLock lock;
private Condition condition;
public PrintOdd(ReentrantLock lock, Condition condition, Wrapper wrapper) {
this.lock = lock;
this.condition = condition;
this.wrapper = wrapper;
}
@Override
public void run() {
while (wrapper.getCount() <= 100) {
lock.lock();
try {
if (wrapper.getCount() % 2 == 0) {
condition.await();
} else {
System.out.println("PrintOdd thread print..." + wrapper.getCount());
wrapper.setCount(wrapper.getCount() + 1);
condition.signal();
}
} catch (Exception ex) {
ex.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
static class PrintEven implements Runnable {
private volatile Wrapper wrapper;
private ReentrantLock lock;
private Condition condition;
public PrintEven(ReentrantLock lock, Condition condition, Wrapper wrapper) {
this.lock = lock;
this.condition = condition;
this.wrapper = wrapper;
}
@Override
public void run() {
while (wrapper.getCount() <= 100) {
lock.lock();
try {
if (wrapper.getCount() % 2 == 1) {
condition.await();
} else {
System.out.println("PrintEven thread print..." + wrapper.getCount());
wrapper.setCount(wrapper.getCount() + 1);
condition.signal();
}
} catch (Exception ex) {
ex.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
}
总结
使用synchronized关键字的时候,要保证被锁住的对象不会发生变更。本文所使用的Integer对象以及String之类的基本对象,执行过程中都容易发生对象变更,synchronized使用时要选择不变化的对象。