java 多线程 原子性和易变性的理解(还有可视性)

本文深入探讨了Java多线程环境下关于原子性、易变性和可视性的概念。通过分析JVM指令,揭示了在并发编程中这些特性的重要性及其对线程安全的影响。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

    volatile关键字还确保了应用中的可视性。如果你将一个域声明为volatile的,那么只要对这个域产生了写操作,那么所有的读操作就都可以
看到这个修改。即便使用了本地缓存,情况也确实如此,volatile域会立即被写入到主存中,而读取操作就发生在主存中。
    理解原子性和易变性是不同的概念这一点很重要。在非volatile域上的原子操作不必刷新到主存中去,因此其他读取该域的任务也不必看到这
个新值。如果多个任务在同事访问某个域,那么这个域就应该是volatile的,否则,这个域就应该只能经由同步来访问。同步也会导致主存中刷新,
因此如果一个域完全由synchronized方法或语句块来防护,那就不必将其设置为是volatile的。
    一个任务所作的任何写入操作对这个任务来说都是可视的,因此如果它只需要在这个任务内部可视,那么你就不需要将其设置为volatile的。
    当一个域的值依赖于它之前的值时(例如递增一个计数器),volatile就无法工作了。如果某个域的值受到其他域的值的限制,那么volatile
也无法工作,例如Range类的lower和upper边界就必须遵循lower<=upper的限制。
    使用volatile而不是synchronized的唯一安全的情况是类中只有一个可变的域。再次提醒,你的第一个选择应该是使用synchronized关键字,
这是最安全的方式,而尝试其他任何方式都是有风险的。
    什么才属于原子操作呢?对域中的值做赋值和返回操作通常都是原子性的,但是,在C++中,i++;i+=2都可能是原子性的。但是在C++中,这要
取决于编译器和处理器。你无法编写依赖于原子性的C++跨平台代码,因为C++没有像Java(在Java SE5中)那样一致的内存模型。

    在Java中,上面的操作可能不是原子性的,正如下面的方法所阐释的JVM指令中可以看到的那样:

    //concurrency / Atomicity.java
    //{Exec: javap -c Atomicity}
    public class Atomicity{
		int i;
		void f1(){i++;}
		void f2(){i+=3;}
    }/* Output:(Sample)
	....
	void f1():
		Code:
			0:		aload_0
			1:		dup
			2:		getfield		#2; //Field i:I
			5:		iconst_1
			6:		iadd
			7:		putfield		#2; //Field i:I
			10:	return

	void f2():
		Code:
			0:		aload_0
			1:		dup
			2:		getfield		#2;  //Field  i:I
			5:		iconst_3
			6:		iadd
			7:		putfield		#2:  //Field  i:I
			10:	return
	*/

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**每条指令都会产生一个get和put,它们之间还有一些其他的指令。因此在获取和放置之间,
 * 另一个任务可能会修改这个域,所以,这些操作不是原子性的:
 * 如果你盲目地应用原子性概念,那么就会看到在下面程序中的getValue()符合上面的描述:
 * 该程序降到奇数值并终止。
 * 
 * 解决办法getValue()和evenIncrement()必须是synchronized的。
 * 在诸如此类情况下,只有并发专家才有能力进行优化,而你还是应该运用Brian的同步规则。
 * 
 * @create @author Henry @date 2016-11-28
 *
 */
public class AtomicityTest implements Runnable {
	/**
	 * 由于i也不是volatile的,因此还存在可视性问题。
	 */
	private int i=0;
	/**
	 * 尽管return i 确实是原子性操作,但是缺少同步使得其数值可以子在处于不稳定的中间
	 * 状态时被读取。
	 * @return
	 */
	public int getValue(){return i;}
	
	private synchronized void evenIncrement(){i++;i++;}
	
	@Override
	public void run() {
		while(true)
			evenIncrement();
	}
	public static void main(String[] args) {
		ExecutorService exec=Executors.newCachedThreadPool();
		AtomicityTest at=new AtomicityTest();
		exec.execute(at);
		while(true){
			int val=at.getValue();
			if(val%2!=0){
				System.out.println(val);
				System.exit(0);
			}
		}
	}
}

/**
 *   SerialNumberGenerator与你想象的一样简单,如果你有C++或其他低层语音的背景,那么可能会期望
 * 递增是原子性操作,因为C++递增通常可以作为一条微处理器指令来实现(尽管不是以任何可靠的、跨平台的形式实现)。
 * 然而正如前面注意到的,Java递增操作不是原子性的,并且涉及一个读操作和一个写操作,所以即使是在这么简单的操作中,
 * 也为产生线程问题留下了空间。正如你所看到的,易变性在这里实际上不是什么问题,真正的问题在于nextSerialNumber()
 * 在没有同步的情况下对共享可变值进行了访问。
 *   基本上,如果一个域可能会被多个任务同时访问,或者这些任务中至少有一个是写入任务,那么你就应该将这个域设置为
 * volatile的。如果你将一个域定义为volatile,那么它就会告诉编译器不要执行任何移除读取和写入操作的优化,
 * 这些操作的目的是用线程中的局部变量维护对这个域的精确同步。实际上,读取和写入都是直接针对内存,而却没有被缓存。
 * 但是,volatile并不能对递增不是原子性操这一事实产生影响。
 * 
 * @create @author Henry @date 2016-11-29
 *
 */
public class SerialNumberGenerator {
	private static volatile int serialNumber=0;
	public static int nextSerialNumber(){
		return serialNumber++;
	}
}

import java.util.Iterator;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

// : concurrency/SerialNumberChecker.java
// Operations that may seem safe are not.
// when threads are present.
// {Args:4}
/**
 * 为了测试SerialNumberGenerator,我们需要不会耗尽内存的集(Set),以防需要花费很长的时间来探测问题。
 * 这里所示的GircularSet重用了存储int数值的内存,并假设在你生成序列数时,产生数值覆盖冲突的可能性极小。
 * add()和contains()方法都是synchronized,以防止线程冲突。
 * 
 * Reuses storage so we don't run out of memory:
 * 
 * @create @author Henry @date 2016-11-29
 */
class CircularSet {
	private int[] array;
	private int len;
	private int index = 0;

	public CircularSet(int size) {
		array = new int[size];
		len = size;
		// Initialize to a value not produced
		// by the SerialNumberGenerator
		for (int i = 0; i < size; i++)
			array[i] = -1;
	}

	public synchronized void add(int i) {
		array[index] = i;
		// Wrap index and write over lod elements;
		index = ++index % len;
	}

	public synchronized boolean contains(int val) {
		for (int i = 0; i < len; i++)
			if (array[i] == val)
				return true;
		return false;
	}
}

/**
 * SerialNumberChecker包含一个静态的CircularSet,它持有所产生的所有序列数;另外还包含一个内嵌的SerialChecker类,
 * 它可以确保序列数是唯一的。通过创建多个任务来竞争序列数,你将发现在和谐任务最终会得到重复的序列数,如果你运行的时间
 * 足够长的话。为了解决这个问题,在nextSerialNumber()前面添加了synchronized关键字。
 * 
 * 对基本类型的读取和赋值操作被认为是安全的原子性操作。但是,正如你在AtomicityTest.java中看到,当对象处于不稳定状态时,
 * 仍旧很可能使用原子性操作来访问它们。对这个问题做出假设是棘手而危险的,最明智的做法就是遵循Brian的同步规则。
 * 
 * @create @author Henry @date 2016-11-29
 * 
 */
public class SerialNumberChecker {
	private static final int SIZE = 10;
	private static CircularSet serials = new CircularSet(1000);
	private static ExecutorService exec = Executors.newCachedThreadPool();

	static class SerialChecker implements Runnable {

		@Override
		public void run() {
			while (true) {
				int serial = SerialNumberGenerator.nextSerialNumber();
				if (serials.contains(serial)) {
					System.out.println("Duplicate: " + serial);
					System.exit(0);
				}
				serials.add(serial);
			}
		}
	}
	/**
	 * 运行结果:
	 * No duplicates detected
	 * Duplicate: 3920303
	 * Duplicate: 3920302
	 * 
	 * @param args
	 * @throws Exception
	 */
	public static void main(String[] args) throws Exception {
		for (int i = 0; i < SIZE; i++) {
			exec.execute(new SerialChecker());
			// Stop after n seconds if there's an argument;
			if (true) {// args.length>0
				TimeUnit.SECONDS.sleep(new Integer("4"));// args[0]
				System.out.println("No duplicates detected");
				//System.exit(0);
			}
		}
	}
}


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值