记得最初使用synchronized关键字时是为了singleton。
仅仅是判断field是不是null,如果为null则指向新的实例。
但这种check-and-action的方式会有同步的问题,也就是说"同时"有两个线程通过了check。
通过这种体验得出了最基本的结论:synchronized可以保证在同一时刻只有一个线程可以执行某个代码块。
很多人把同步的概念仅仅理解为互斥关系(mutual exclusion)。
比如,当一个对象被一个线程修改的时候可以阻止其他线程观察到该对象的状态。
即,对象创建之初是一致状态,被访问时则被锁定,此时其状态可能会改变,但最终还是会保持一致状态。
但这并不是synchronized的全部意义。
另一个意义在于,同步可以保证进入同步代码块的线程可以看到之前线程的修改结果,前提是由同一个锁进行保护。
JLS 17.4.7 http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html 讲道读写一个非double非long的变量是原子的,即多个线程在没有同步的情况下安全地修改这个变量,但是这并不代表一个线程写入的值对另一个线程是可见的。
所以为了可靠通信,我们仍需要进行同步。
以下面的代码为例,让一个线程轮询一个boolean field,false时继续执行,接着让主线程将其变成true,以让轮询终止:
import java.util.concurrent.TimeUnit; public class StopThread { private static boolean stopRequested; public static void main(String[] args) throws InterruptedException { Thread backgroundThread = new Thread(new Runnable() { public void run() { int i = 0; while (!stopRequested) i++; } }); backgroundThread.start(); TimeUnit.SECONDS.sleep(1); stopRequested = true; } }
遗憾的是主线程将stopRequested设为true并没有让backgroundThread停下来。
正如之前所说,问题在于没有进行同步backgroundThread感觉不到其他线程已经修改了stopRequested。
也就是说
while(!stopRequested)i++;
相当于
if(!stopRequested) while(true)i++;
于是,我们为stopRequested提供一个读方法并加上synchronized,如下:
public class StopThread { 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() { public void run() { int i = 0; while (!stopRequested()) i++; } }); backgroundThread.start(); TimeUnit.SECONDS.sleep(1); requestStop(); } }
需要注意的是,写方法也需要进行同步,读写操作都需要加上synchronized关键字才会起作用。
遗憾的是,到这一步为止并没有合理使用synchronized关键字。
我们所做的这些不是为了互斥访问,因为读写方法中的行为即使没有加上synchronized关键字也是原子的,我们想要的效果仅仅是保证线程之间的通信效果。
我们应该有更正确的方式解决这个问题,于是将代码改成如下形式:
import java.util.concurrent.TimeUnit; public class StopThread { private static volatile boolean stopRequested; public static void main(String[] args) throws InterruptedException { Thread backgroundThread = new Thread(new Runnable() { public void run() { int i = 0; while (!stopRequested) i++; } }); backgroundThread.start(); TimeUnit.SECONDS.sleep(1); stopRequested = true; } }
虽然volatile关键字并不保证互斥访问,但可以保证线程可以感觉到其他线程之前的操作结果。
但是,volatile关键字也有使用不当的时候,比如下面的代码想每次都返回不同的值:
private static volatile int nextSerialNumber = 0; public static int generateSerialNumber(){ return nextSerialNumber++; }
这段代码运行正常是运气好,因为不用于之前的boolean类型变量赋值,++操作不是原子的,它需要先读取旧值再对其进行增加再返回新值。
这就像最初check-and-action的问题,多个线程同时读到了相同的旧值。
虽然synchronized关键字可以解决这一问题,但我们有更好的选择:
private static final AtomicLong nextSerialNum = new AtomicLong(); public static long generateSerialNum(){ return nextSerialNum.getAndIncrement(); }