如何在我的代码中实现线程安全

时间:2012-07-30 16:39:09

标签: java concurrency thread-safety

假设我有一个 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();
        }
    }
}

6 个答案:

答案 0 :(得分:4)

  

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

想象一下以下执行:

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

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

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

<强>解决方案

根据您的目标,有几种选择:

  • 你可以将这2行放在synchronized(lock)块中,使2个调用原子
  • 您可以将参数传递给printMessage方法:printMessage(message),删除共享变量问题
  • 您可以为每次调用创建一个类实例,同时删除共享变量问题
  • ...

答案 1 :(得分:2)

这是你的问题:

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

这两个操作不是原子操作,只有printMessage()。所以偶尔会发生这种情况:

  1. 主题A修改message字段
  2. 线程B也会启动并修改它
  3. 线程B继续运行,呼叫printMessage()
  4. 线程B中的
  5. printMessage()完成并清理message字段
  6. 线程A已恢复并调用printMessage()。发生灾难
  7. 如果您希望代码是线程安全的,那么这两个操作必须是原子的。很难提出建议,因为您的代码中存在其他几个问题:公共可变字段,可见性问题,锁定是不必要的静态...

    如果您可以修改此伪代码,我只需将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的情况。