假设我有一个 Base 类,其中包含 消息 (字符串)。另一个类 BaseHandler 扩展了此 Base 类。在这个处理程序中,我有一个方法 print ,它正在为基础设置一个值并打印它。在打印调用结束时,我将消息设置为null。 当我创建50000个线程并运行处理程序的print方法时,我偶尔会得到空指针异常。
问题:
为什么在显式分配值时抛出空指针异常?
在这种情况下,每个线程如何实例化Base?
解决方案是否将Base.message标记为volatile并删除null赋值? (换句话说,如何在 Base.message
请看下面的代码:
public class Base {
public String message;
}
public class BaseHandler extends Base{
protected static final Object lock = new Object();
public void printMessage( ){
synchronized ( lock ) { //This block is thread safe
System.out.println( message.toUpperCase( ) );
message = null;
}
}
}
public class Test {
public static void main(String[] args){
final BaseHandler handler = new BaseHandler();
for (int i = 0; i < 50000; i++) {
Runnable task = new Runnable(){
@Override
public void run( ) {
handler.message = "Hello world! ";
handler.printMessage( );
}
};
Thread worker = new Thread(task);
worker.setName(String.valueOf(i));
worker.start();
}
}
}
答案 0 :(得分:4)
为什么在显式分配值时抛出空指针异常?
想象一下以下执行:
handler.message = "Hello world! ";
handler.message = "Hello world! ";
message.toUpperCase()
抛出NPE。你的问题是下面的2行不是原子的:
handler.message = "Hello world! ";
handler.printMessage();
<强>解决方案强>
根据您的目标,有几种选择:
synchronized(lock)
块中,使2个调用原子printMessage(message)
,删除共享变量问题答案 1 :(得分:2)
这是你的问题:
handler.message = "Hello world! ";
handler.printMessage( );
这两个操作不是原子操作,只有printMessage()
。所以偶尔会发生这种情况:
message
字段printMessage()
printMessage()
完成并清理message
字段printMessage()
。发生灾难如果您希望代码是线程安全的,那么这两个操作必须是原子的。很难提出建议,因为您的代码中存在其他几个问题:公共可变字段,可见性问题,锁定是不必要的静态...
如果您可以修改此伪代码,我只需将message
作为printMessage()
的参数传递(听起来合理),并忘记线程安全和多线程。代码是安全的。
答案 2 :(得分:1)
您正在锁定读取,但是在更新值时需要锁定。你的锁需要包含两者以实现线程安全:即锁定这些。
handler.message = "Hello world! ";
handler.printMessage( );
答案 3 :(得分:1)
消息的设置在您的代码中不是线程安全的......
public class MyClass implements Runnable{
BaseHandler handler = new BaseHandler();
public synchronized void go(){
for (int i = 0; i < 50000; i++) {
handler.message = "Hello world! ";
handler.printMessage( );
}
}
}
现在让你Base和BaseHandler一样...... 对测试类的修改很少
public class Test {
public static void main(String[] args){
Thread worker = new Thread(MyClass);
worker.setName(String.valueOf(i));
worker.start();
}
}
答案 4 :(得分:1)
您需要确保共享可变对象的读取和写入操作的线程安全性。在你的情况下,你正在做一个不安全的写作。此外,读取和写入需要共享相同的锁定。
public synchronized void setMessage(String msg) {
this.message = msg;
}
public synchronized String getMessage() {
return message;
}
}
请注意,这里我隐式使用对象实例作为锁。将不同的对象用作同一可变对象的锁是一个常见的错误。
然后,您的BaseHandler类将如下所示:
public class BaseHandler extends Base {
public synchronized void printMessage( ) {
if (getMessage()!=null) {
System.out.println( getMessage().toUpperCase( ) );
setMessage(null);
}
}
}
这两种方法使您的Base-BaseHandler类层次结构对任何客户端都是线程安全的。 这意味着使用您的对象的客户端不需要使用同步。
答案 5 :(得分:1)
如果您只是将堆栈状态作为参数传递,那么您正在使用脆弱的扩展和共享的可变状态来实现更简单的操作。 make printMessage将消息作为参数来解决所有这些问题。
public void printMessage(final String message){
System.out.println( message.toUpperCase( ) );
}
现在你只需要处理实际传递null的情况。