问题
并发程序要比单线程程序的设计更加复杂,并且失败难以重现。但是又无法避免采用多线程,因为采用多线程并发是能够从多核计算机获得最佳性能的一个有效途径。在并发时,如果涉及到可变数据的时候,就是我们需要着重去思考的地方,在面对可变数据的并发访问的时候,有哪些方式可以保证线程安全性?
答案
关键字synchronized:synchronized是保证线程安全的一大利器,它可以保证同一时刻,只有一个线程可以执行某个方法和修改某一个可变数据,但是仅仅将它理解成是互斥的也是不完全正确的,它主要有两种意义:
- 当一个对象被一个线程修改的时候,可以阻止另一个线程观察到对象内部不一致的状态;
- 同步不仅可以阻止一个线程看到对象处于不一致的状态,还可以保证进入同步方法或者同步代码块的每个线程,都看到由同一个锁保护的之前所有的修改效果。
另外,java语言规范保证读写一个变量是原子的,除非这个变量是double或者long,即使没有在保证同步的情况下也是如此。
考虑到这样一个示例,线程通过轮询标志位而达到优雅的停止线程的功能,示例代码如下:
private static boolean stopRequested; private static synchronized void requestStop() { stopRequested = true; } private static synchronized boolean stopRequested() { return stopRequested; } public static void main(String[] args) throws InterruptedException { Thread backgroundThread = new Thread(new Runnable() { @Override public void run() { int i = 0; while (!stopRequested()) { i++; } } }); backgroundThread.start(); TimeUnit.SECONDS.sleep(1); requestStop(); }
可变数据也就是状态变量stopRequested,被同步方法修改,这里也就是保证stopRequested被修改后,能够被其他线程立即可见。
关键字volatile:volatile最重要的功能是能够保证数据可见性,当一个线程修改可变数据后,另一个线程会立刻知道最新的数据。在上面的例子中,因为stopRequested变量的读写本身就是原子的,因此利用synchronized只是利用到它的数据可见性,但是由于synchronized会加锁,如果想性能更优的话,上面的例子就可以采用volatile进行修改:
private static volatile boolean stopRequested; public static void main(String[] args) throws InterruptedException { Thread backgroundThread = new Thread(new Runnable() { @Override public void run() { int i = 0; while (!stopRequested) { i++; } } }); backgroundThread.start(); TimeUnit.SECONDS.sleep(1); stopRequested = true; }
但是需要注意到volatile并不能保证原子性,例如下面的例子:
private static volatile int nextSerialNumber = 0; public static int generateSerialNumber() { return nextSerialNumber++; }
尽管使用了volatile,但是由于++运算符不是原子的,因此在多线程的时候会出错。++运算符执行两项操作:1、读取值;2、写回新值(相当于原值+1)。如果第二个线程在第一个线程读取旧值和写会新值的时候读取了这个域,就会产生错误,他们会得到相同的SerialNumber。这个时候就需要使用synchorized来使得线程间互斥访问,从而保证原子性。
总结
解决这一问题的最好办法其实是尽量避免在线程间共享可变数据,将可变数据限制在单线程中。如果想要多个线程共享可变数据,那么读写都需要进行同步。