多线程中主存与线程工作空间同步数据的时机

在测试volatile关键字如何保证数据在多个线程中的可见性问题的时候,引发的思考!

对于一个临界资源,如果使用volatile关键字修饰,那么就可以保证该变量在多个线程中可见。对于原理的理解不是很难,但是使用到代码来模拟多线程问题的时候,对于何时从主存读取共享变量何时将工作内存刷写到主存的时机却不是特别清楚。导致对于多线程理解不够透彻!

1、线程的工作内存刷写到主存以及从主存读取到工作内存的时机

问题描述: 一个线程何时会从主存中去重新读取共享变量的值,又是何时需要将工作内存的值重新刷写到主存中。
前提: 不使用volatile关键字保证可见性的情况

主存和工作内存说明:

在多线程中,多个线程访问主存中的临界资源(共享变量)时,需要首先从主存中拷贝一份共享变量的值到自己的工作内存中,然后在线程中每次访问该变量时都是访问的线程工作内存(高速缓存)中的共享的变量副本,而不是每次都去主存中读取共享变量的值(因为CPU的读写速率和主存读写速率相差很大,如果CPU每次都访问主存的话那么效率会非常低)。当线程结束、IO操作导致线程切换、抛出异常等情况发生时会将自己工作内存中的值刷写到主存中。

如果每个线程都使用自己工作内存中的共享变量值,而不去读取主存中的值(主存中的共享变量值可能已经被修改),那么就会造成多线程的数据同步问题。多线程中使用 volatile 关键字可以解决数据同步的问题。但 是现在要讨论的问题是如果不使用volatile关键字,那么什么时候线程会重新去主存中读取共享变量的值以及什么时候会将工作内存刷写会主存呢?

public class TestVolatitle1 {

	public static void main(String[] args) throws InterruptedException {
		ThreadDemo td = new ThreadDemo();
		Thread threadA = new Thread(td);
		threadA.start();

		while (true) {
			//System.out.println("打开这句以后,会进入if条件中,程序可正常结束!");
            //或者是打开下面这句
            //Thread.sleep(1);
			if (td.isFlag()) {
				System.out.println("------------------");
				break;
			}
		}
	}
}
class ThreadDemo implements Runnable {

	private boolean flag = false;

	@Override
	public void run() {

		try {
			Thread.sleep(10);
		} catch (InterruptedException e) {

		}
		flag = true;
		System.out.println("flag=" + isFlag());
	}

	public boolean isFlag() {
		return flag;
	}

	public void setFlag(boolean flag) {
		this.flag = flag;
	}
}

代码问题说明:

从上面这个测试程序中来看,简单来讲其实这就是一个可见性问题。共享变量 flag没有使用 volatile关键字修饰,所以没有线程间的内存可见性。控制台输出的结果为: flag=true然后卡住,程序结束不了。

但是当放开那句注释掉的语句以后,程序可以正常结束。这说明了使用 System.out.println语句会导致线程重新从主存中读取共享变量的值到工作内存中。这里并没有使用volatile关键字去修饰共享变量,仅仅使用了一下System.out.println语句。所以flag变量在各个线程中仍然不具有可见性,所以这里主线程仍然不知道主线程的flag值已经改变,但是一定有某种原因使得主线程强制去主存中重新读取了flag的值。

这是什么原因导致的呢?

1、网上有一种说法是因为 System.out.println方法是一个用 synchronized 关键字修饰的同步方法。当执行完这个方法以后会释放锁,释放锁的操作会导致该线程重新从主存中读取共享变量的值。(线程释放锁会强制刷写到主存,线程获取锁会强制从主存重新刷新变量值)

从本质上来说,==当线程释放一个锁时会强制性的将工作内存中之前所有的写操作都刷新到主内存中去,而获取一个锁则会强制性的加载可访问到的值到线程工作内存中来。==虽然锁操作只对同步方法和同步代码块这一块起到作用,但是影响的却是线程执行操作所使用的所有字段。

2、第二种说法是因为System.out.println方法是一个IO操作,IO操作会引起线程的切换,而线程的切换会导致线程原本的工作内存中缓存失效,然后去主存重新读取共享变量的值(当线程切换到其他线程,后来又切换到该线程的时候去重新读取)。为了验证是否IO操作会引起线程切换并且缓存失效,将打印语句换成: File file = new File("D://temp.txt");测试结果依然会让程序正常结束。

3、自己添加了一句 Thread.sleep(1);代码在主线程中的while循环中。结果程序仍然可以正常结束。针对于这种情况我在stack Overflow中看到了一个回答,说的是当执行该线程的cpu有空闲时,他会去主存读取一下共享变量的值来更新线程工作内存中的值(意思就是说,在这里我使用 sleep 使得执行该线程得CPU有了1毫秒的空闲时间,在这一毫秒的空闲时间中CPU重新去主存中读取了共享变量的值)。如果按照这种说法,那么就是说CPU何时去主存中重新读取共享变量刷新缓存是一个不确定的因素(CPU有空闲时间就可以去重新读取)。

问题: 在不使用volatile关键字的情况下,有哪些情况会导致线程的工作内存失效,然后必须重新去读取主存的共享变量?

1、线程中释放锁时

2、线程切换时

3、CPU有空闲时间时(比如线程休眠时)

类似问题:

public class Test01 implements Runnable {

	private int count = 10;

	@Override
	public /*synchronized*/ void run() {
		count--;
        //下面的语句涉及到IO操作,所以会导致线程切换,从而重新去主存读取count值
		System.out.println(Thread.currentThread().getName() + " count = " + count);
	}

	public static void main(String[] args) {
		Test01 t = new Test01();
		for (int i = 0; i < 5; i++) {
			new Thread(t).start();
		}
	}
}

输出结果:

Thread-0 ount = 7
Thread-3 ount = 5
Thread-4 ount = 6
Thread-1 ount = 7
Thread-2 ount = 7

这个结果不是固定的。造成这个问题的原因是,同时开启了五个线程,五个线程把主存中的值拷贝到线程的工作内存中,然后执行 run() 方法在该方法中对count进行减一。遇到 System.out.println语句后引起线程切换。导致了将工作内存中的count刷写回主存,然后执行 System.out.println时重新从主存中读取值。如果对run方法加锁即可解决该问题。

你可能感兴趣的:(java常见知识)