许多程序员把同步的概念仅仅理解为一个种互斥的方式,即,当一个对象被一个线程修改的时候,可以阻止另一个线程观察到对象的内部不一致的状态。正确地使用同步可以保证其他任何方法都不会看到对象处于不一致的状态中。这种观点是正确的,但是它并没有说明同步的全部意义。如果没有同步,一个线程的变化就不能被其他线程看到。同步不仅可以阻止一个线程看到对象处于不一致的状态中(即原子性),它还可以保证进入同步方法或者同步代码块的每个线程,都看到由同一个锁保护的之前所有的修改结果(即可见性)。
我的理解,同步=原子性+可见性
synchronized就是同步的代名词,它具有原子性与可见性。而volatile只具有可见性,但不具有原子性。可见性其实说的就是在读之前与写之后都与主内同步,除了可见性外,volatile还严禁语义重排:“禁止reorder任意两个volatile字段或者volatile变量,并且同时严格限制(尽管没有禁止)reorder volatile字段(或变量)周围的非volatile字段(或变量)。”
Java语言规范保证或写是一个变量是原子的(即数据的读写是不可分割的。注,不可分割的操作并不意味“多线程安全”),除非这个变量的类型为long或double[JLS 17.4.7]。换句话说,读取一个非long或double类型的变量,可以保证返回值是某个线程完整保存在该变量中的值(即要么读取还没有修改的值,要么读取到某线程修改完后的值,但决不会读到另一线程对变量的一半或一部分修改后的值,如一个int型变量,某线修改该变量的前16位后,被另一线程读到,这是不可能的;而long或double类型的变量就完全有可能这样,读到的是另一线程写入的高32位,而低32位还是原来值),即使用多个线程在没有同步的情况下并发地修改这个变量也是如此。
你可能听说过,为了提高性能,在读或写原子数据的时候,应该避免使用同步。这个建议是非常危险而错误的。虽然语言规范保证了线程在读取原子数据的时候,不会看到任意的数值(严格的说是完整的值,即不会读取还未修改完成的值),但是它并不保证一个线程写入的值对于另一个线程将是可见的(即另一线程修改完后,其他线程有可能将永远读不到这个修改后的值)。为了在线程之间进行可靠的通信(需要靠可见性来保证),也为了互斥访问(需要原子性来保证),同步(需要可见性和原子性来保证)是必要的。这归因于Java语言规范中内存模型,它规定了一个线程所做的变化何时以及如何让其他线程可见[JLS 17]。
如果对共享的可变数据的访问不能同步,其后果将非常可怕,即使这个变量是原子可读写的。考虑下面这个阻止一个线程妨碍另一个线程的任务。由于boolean域的读和写操作都是原子的,程序员在访问这个域的时候不再使用同步,这是错误的做法:
importjava.util.concurrent.TimeUnit;
publicclassStopThread {
PRivatestaticbooleanstopRequested;
publicstaticvoidmain(String[] args)throwsInterruptedException {
Thread backgroundThread =newThread(newRunnable() {
publicvoidrun() {
inti = 0;
while(!stopRequested)
i++;
}
});
backgroundThread.start();
//睡一秒
TimeUnit.SECONDS.sleep(1);
stopRequested=true;
}
}
你可能期待这个程序运行大约一秒钟之后,主线程将stopRequested设置为true,致使后台线程的循环终止。但是在我的机子上,这个程序永远不会终止:因为后台线和永远在循环中!
问题在于,由于没有同步,就不能保证后台线程何时“看到”主线程对stopRequested的值所做的修改。在没有同步的情况下,VM将个这个代码:
while(!stopRequested)
i++;
转变成这样:
if(!stopRequested)
while(true)
i++;
这是完全有可能的,也是可以接受的。这种优化称作提升(hoisting),正是HopSpot Server VM的工作。结果是个“活性失败”:这个程序无法结束。修改这个问题的一种方式是同步访问stopRequested域,修改如下:
publicclassStopThread {
privatestaticbooleanstopRequested;
privatestaticsynchronizedvoidrequestStop() {
stopRequested=true;
}
privatestaticsynchronizedbooleanstopRequested() {
returnstopRequested;
}
publicstaticvoidmain(String[] args)throwsInterruptedException {
Thread backgroundThread =newThread(newRunnable() {
publicvoidrun() {
inti = 0;
while(!stopRequested())
i++;
}
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
requestStop();
}
}
注意上面的写方法(requestStop)和读方法(stopRequested)都被同步了,只同步写方法或读方法是不够的!
StopThread程序中被同步方法的动作即使没有同步也是原子的。换句话说,这些方法的同步只是为了它的通信效果(即可见性),而不是为了互斥访问(即原子性)。虽然循环的每个迭代中的同步开销很小,还是有其他更正确的替代方法,它更加简洁,性能也可能更好。这种替代就是将stopRequested声明为volatile,第二版本的StopThread中的锁就可以省略。虽然volatile修饰符不具有互斥访问的特性,但它可以保证任何一个线程在读取该域的时候都将看到最近刚刚被其他线程写入的值,下面是使用volatile修正后的版本:
publicclassStopThread {
privatestaticvolatilebooleanstopRequested;
publicstaticvoidmain(String[] args)throwsInterruptedException {
Thread backgroundThread =newThread(newRunnable() {
publicvoidrun() {
inti = 0;
while(!stopRequested)
i++;
}
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested=true;
}
}
上面就说了,volatile只具有可见性,而不具有原子性,所以使用时要格外小心,请考虑下面的方法,假设它要产生序列号:
privatestaticvolatileintnextSerialNumber= 0;
publicstaticintgenerateSerialNumber() {
returnnextSerialNumber++;
}
这个方法的目的是要确保每次调用都要返回不同的值,而且是递增的(只要不超过2^32次调用)。这个方法的状态只包含一个可原子访问的域:nextSerialNumber,不同步的情况下读到的这个域的所有可能的值都是合法(即不可能读到修改未完成的值),但是,这个方法仍然无法工作。
问题在于,增量操作(++)不是原子的。它在nextSerialNumber域中执行两项操作:首先它读取值,然后写回一个新值,相当于原来的值再加上1。如果第二个线程在第一个线程读取旧值和写回新值期间读取这个域,第二个线程就会与第一个线程一起看到同一个值,并返回相同的序列号。这就是“安全性失败”:这个程序会计算出错误的结果。
修正generateSerialNumber方法的一种方法是是在它的声明中加上synchronized修饰符。这样可能确保多个调用不会交叉存在。一旦这么做,就可以且应该从nextSerialNumber中删除volatile修饰符。为了让这个方法更可靠,要用long代替int。但最好还是遵循第47条中的建议,使用类AtomicLong,它是java.util.concurrent.atomic的一部分,它比同步版本的generateSerialNumber性能上可能要更好,因为atomic包使用了非锁定的线程安全技术来做到同步的,下面是使用AtomicLong修正后的版本:
privatestaticfinalAtomicLongnextSerialNum=newAtomicLong();
publicstaticlonggenerateSerialNumber() {
returnnextSerialNum.getAndIncrement();
}
避免本条目中所讨论到的问题的最佳办法是不共享可变的数据,要么共享不可变的数据(见第15条),要么压根不共
新闻热点
疑难解答