Effective Java 读书笔记——66:同步访问共享的可变数据

来源:互联网 发布:免费网络宣传 编辑:程序博客网 时间:2024/05/26 02:19

关键字synchronized可以保证同一时刻,只有一个线程可以执行某个方法。

同步的概念

1、当一个对象被一个线程修改的时候,可以阻止另一个线程观察到对象内部不一致的状态;

2、同步不仅可以组织一个线程看到对象处于不一致的状态,还可以保证进入同步方法或者同步代码块的每个线程,都看到由同一个锁保护的之前所有的修改效果。

另外,java语言规范保证读写一个变量是原子的,除非这个变量是double或者long(JSL,17.4.7),即使没有在保证同步的情况下也是如此。


"为了提高性能,在读写原子数据的时候,应该避免使用同步。”这个建议是非常危险而且错误的。因为,虽然读写原子数据都是原子操作,但是不保证一个线程的写入的值对于另一个线程是完全可见的(值得一提的是,究竟什么样的原子变量必须进行同步,是需要看情况的)。因此,为了在线程之间进行可靠的通信,也为了互斥访问,同步是必要的。


考虑一个线程妨碍另一个线程任务

如果需要停止一个线程,可以使用Thread.stop方法,但是这个方法很久以前就不提倡使用了,因为不安全——使用它会使数据被破坏。
因此,建议的做法是,让一个线程轮询一个boolean域,另一个线程设置这个boolean域即可:
public class Temp {private static boolean stopRequested;public static void main(String[] args) throws InterruptedException {Thread backgroundThread = new Thread(new Runnable() {@Overridepublic void run() {int i = 0;while (!stopRequested) {i++;}}});backgroundThread.start();TimeUnit.SECONDS.sleep(1);stopRequested = true;}}
实际上以上这段程序会永远的运行下去,因为没有使用同步,无法保证后台进程可以看到stopRequested值的改变。虚拟机将代码:
while (!stopRequested) {i++;}
转变成了:
if(!stopRequested)    while(true)        {            i++;        }
这样一来,永远都不会看到stopRequested的改变。
必须让线程看到变量的改变才好,因此使用同步修改以上的代码:
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() {@Overridepublic void run() {int i = 0;while (!stopRequested()) {i++;}}});backgroundThread.start();TimeUnit.SECONDS.sleep(1);requestStop();}
事实上,在本例中,只同步读方法(这和下面的volatile类似),不同步写方法,也是可以的。

使用volatile

在上面的例子里面,使用同步进行的,我们同步是为了保证通信效果,而不是互斥,所以可以使用volatile来实现相同的功能:
private static volatile boolean stopRequested;public static void main(String[] args) throws InterruptedException {Thread backgroundThread = new Thread(new Runnable() {@Overridepublic 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。
解决方法可以是,加入synchronized并去掉volatile。进一步的,可以用Long来代替int,或者在快要溢出的时候,抛出异常。更好的是使用AtomicLong类。
private static final AtomicLong nextSerialNumber = new AtomicLong(0);public static Long generateSerialNumber() {return nextSerialNumber.incrementAndGet();}


总结

解决这一问题的最好办法其实是尽量避免在线程间共享可变数据,将可变数据限制在单线程中。让线程短时间修改一个对象,并与其他线程共享,这是可以接受的,只需要同步修改的动作即可。不需要同步,其他线程也可以读取该对象,只要它以后没有再改变。总的来说,如果想要多个线程共享可变数据,那么读写都需要进行同步。


0 1
原创粉丝点击