0

假设我有一个带有单个成员消息(字符串)的类Base 。另一个类BaseHandler扩展了这个Base类。在这个处理程序中,我有一个print方法,它为基数设置一个值并打印它。在打印调用结束时,我将消息设置为空。当我创建 50000 个线程并运行处理程序的打印方法时,我偶尔会收到空指针异常。

问题:
为什么在显式赋值时会抛出空指针异常?
在这种情况下,每个线程将如何实例化 Base?
解决方案会将 Base.message 标记为 volatile 并删除空分配吗?(换句话说,如何在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();
        }
    }
}
4

6 回答 6

4

为什么在显式赋值时会抛出空指针异常?

想象以下执行:

  • 线程1:handler.message = "Hello world! ";
  • 线程2:handler.message = "Hello world! ";
  • Thread1:获取锁,打印并将消息设置为 null
  • thread2:获取锁,尝试打印但message.toUpperCase()抛出 NPE。

您的问题是下面的 2 行不是原子的:

handler.message = "Hello world! ";
handler.printMessage();

解决方案

有几种选择,具体取决于您要实现的目标:

  • 您可以将这 2 行放在一个synchronized(lock)块中,以使 2 个调用原子
  • 您可以将参数传递给 printMessage 方法:printMessage(message),删除共享变量问题
  • 您可以在每次调用时创建一个类的实例,同时消除共享变量问题
  • ...
于 2012-07-30T16:43:41.340 回答
2

这是你的问题:

handler.message = "Hello world! ";
handler.printMessage( );

这两个操作不是原子的,只是原子printMessage()的。所以这是偶尔发生的事情:

  1. 线程 A 修改message字段
  2. 线程 B 也启动并修改它
  3. 线程B继续运行,调用printMessage()
  4. printMessage()在线程 B 中完成并清理message字段
  5. 线程 A 恢复并调用printMessage(). 灾难发生

如果您希望您的代码是线程安全的,那么这两个操作需要是原子的。很难给出建议,因为您的代码中还有其他几个问题:公共可变字段、可见性问题、锁定是不必要的静态......

如果您可以修改此伪代码,我会简单地message作为printMessage()(听起来很合理)的参数传递,而忘记线程安全和多线程。该代码将是安全的。

于 2012-07-30T16:45:31.967 回答
1

您正在锁定读取,但您需要在更新值时锁定。您的锁需要包含两者以实现线程安全:即锁定这些。

handler.message = "Hello world! ";
handler.printMessage( );
于 2012-07-30T16:47:28.463 回答
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 ... 对 Test 类进行少量更改

public class Test {   
    public static void main(String[] args){       


            Thread worker = new Thread(MyClass);
            worker.setName(String.valueOf(i));
            worker.start();
        }
    }
于 2012-07-30T16:48:35.260 回答
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 类层次结构对任何客户端都是线程安全的。这意味着使用您的对象的客户端不需要使用同步。

于 2012-07-30T16:50:38.220 回答
1

如果您只是将状态作为参数在堆栈上传递,您正在使用脆弱的扩展和共享的可变状态来实现更简单的操作。让 printMessage 将消息作为参数,你就解决了所有这些问题。

public void printMessage(final String message){
    System.out.println( message.toUpperCase( ) );
}

现在您只需要处理实际传递了 null 的情况。

于 2012-07-31T00:10:05.250 回答