原题大概是这么个意思。
一个线程A 执行setJob方法(设为方法1),一个线程B执行getJob方法(设为方法2),问下面这段代码有啥问题。
Object lock = new Object();
Object job;
public void setJob(Object job){
this.job = job;
synchronized (lock){
lock.notifyAll();
}
}
public Object getJob(){
Object job = this.job;
if(job==null){
synchronized (lock){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
return null;
}
}
}
return job;
}
**分析:**这里很明显是个整个等待通知机制,想要当A线程对job赋值后,B线程读取job。
写个测试代码看看:
public static void main(String[] args) throws InterruptedException {
Thread t1,t2;
for(int i=0;i<100;i++){
test t = new test();
t1 = new Thread(()->t.setJob(
Thread.currentThread().getName()),
String.valueOf(i));
t2 = new Thread(()->{System.out.println(
t.getJob());});
t1.start();
t2.start();
t1.join();
t2.join();
}
}
很明显不符合预期啊。为啥会出现这个问题呢?没按照设想的来啊。
有两种可能。
- 线程A对job对象的写对线程B写不可见。
- 线程B中的判断无用或者二次读值失败
1.
第一个问题很好解决,给job变量加一个volatile关键字即可,使用内存屏障(写读屏障),迫使读线程去主存获取job的引用,迫使写线程将值写入主存。
加了后会发现还是有null的情况。其实稍作思考就知道,问题不在这,测试代码里赋值操作的执行内容很少,就一个赋值操作不存在内部重排序,而且本身线程并不执行其他操作很快就会主动刷新到主存中,而且线程B也仅仅有一次获取。
不过这是个隐患,所以对于这种需要写读分离且在双方的执行操作都相对复杂的时候(比如一个线程多次读的过程中(比如循环),有写操作发生,而且需要及时感知),较长的变量最好还是加上volatile关键字。
2.
那就锁定到第二个问题上了,这个问题还要从wait方法说起,从wait状态中恢复后会从哪里开始执行呢?
事实上,它被唤醒后,并非重新进入方法(估计出题人的意思就是这个),该线程会重新尝试获取锁,然后从调用wait()方法的下一个指令开始执行(由线程独占的程序计数器实现)。
也就是说方法2恢复后不会再判断一遍if也不会执行一遍if判断也不会将值再读一遍,现在仍然是写入前读取的值,为null。
解决方式: 将wait()之后再添加一个赋值语句。(如果还有其他非写线程的唤醒操作,就再将if换成while)
引申:
如果想要实现多个AB线程之间的交替读写呢?
交替读写自然就需要线程之间相互通信。
只有一个A线程更新了值一个B线程才能去读取。
只有当一个B线程读完了才能去写值。
Object lock = new Object();
String s;
volatile boolean flag =true;
public String setSrting(String s){
synchronized (lock) {
while (!flag) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
flag =false;
this.s = s;
String ss = this.s;
lock.notifyAll();
return ss;
}
}
public String getString(){
synchronized (lock){
while (flag){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
flag = true;
String ss = new String(s);
lock.notifyAll();
return ss;
}
}