欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

同步访问共享的可变数据 博客分类: Effective Java

程序员文章站 2024-03-16 11:15:40
...

如果对共享的可变数据不能同步访问,其后果是非常可怕的,即使这个变量是原子可读写的。

 

在一个线程中终止另一个线程时使用Thread.stop方法是不安全,使用它会遭到数据破坏。通常的做法是轮询一个boolean域:

public class TestCode {

	private static boolean stopRequested=false;
	
	public static void main(String[] args) throws InterruptedException {
		
		
		Thread thread=new Thread(new Runnable() {
			public void run() {
				int i=0;
				while (!stopRequested) {
					i++;
				}
			}
		});
		
		thread.start();
		TimeUnit.SECONDS.sleep(1);
		
		stopRequested=true;
		
	}
}

 

 

 

但是上述代码的问题在于由于没有同步,就不能保证后台线程何时”看到“主线程对stopRequested的值所做的改变,没有同步虚拟机就会将下面这种代码:

 

while (!stopRequested) {
					i++;
}

 优化成

 

 

while (!stopRequested) {
					while (true)
						i++;
}

 这个应该是所谓的指令重排序的以达到性能提升的目的,结果会导致程序失败。

 

 

正确的做法是同步访问stopRequested,代码如下:

 

public class TestCode {

	private static boolean stopRequested = false;

	public static synchronized boolean getStopRequested() {
		return stopRequested;
	}
	public static synchronized  void stopRequest(){
		stopRequested=true;
	}
	

	public static void main(String[] args) throws InterruptedException {

		Thread thread = new Thread(new Runnable() {
			public void run() {
				int i = 0;
				while (!getStopRequested()) {
					i++;
				}
			}
		});

		thread.start();
		TimeUnit.SECONDS.sleep(1);

		stopRequest();

	}
}

 synchronized关键字修饰以上两个方法只是为了达到同步的通信效果,而不是为了互斥,虽然上面这种循环中每次访问的开销很小但是还是有更加高效的做法,使用volatile关键字修饰stopRequested,volatile关键字可以保证任何一个线程在读取该域的时候都将看待最近刚刚被写入的值:

public class TestCode {

	private static  volatile boolean stopRequested=false;
	
	public static void main(String[] args) throws InterruptedException {
		
		
		Thread thread=new Thread(new Runnable() {
			public void run() {
				int i=0;
				while (!stopRequested) {
					i++;
				}
			}
		});
		
		thread.start();
		TimeUnit.SECONDS.sleep(1);
		
		stopRequested=true;
		
	}
}

 

 在使用volatile的时候要务必小心,例如:

 

	private static volatile int nextNumber=1;
	
	public static int generateNumber(){
		return nextNumber++;
	}

 假如上述方法没有被同步是无法正常工作的,因为增量操作++不是原子性的,实际上++进行了两项操作:首先它读取值,然后写回一个新值,相当于在原来的值上再加上1。如果第二个线程在第一个线程读取旧值和写回新值之间读取这个域,第二个线程就会与第一个线程一起看到同一个值。这是安全性失败。虽然这种情况的发生时小概率事件但是还是需要我们注意的。改进的做法是在访问的方法上添加synchronized关键字,同时删去volatile来进行同步以确保多个调用不会交叉存取。其实对于这种情况最好的做法是使用AtomicLong来对generateNumber进行修饰。

     总之,多线程访问共享可变数据的时候读和写数据的线程都必须进行同步,如果没有进行同步就无法保证一个线程的修改被另一个线程所感知,未能同步共享可变数据就会造成活性失败和安全性失败。如果只是线程之间的交互通信那么则不需要进行互斥,使用volatile关键字就可以搞定。