2
回答
多线程下对static变量++操作,为什么使用volatile修饰后的结果比不用小很多?
【腾讯云】校园拼团福利,1核2G服务器10元/月!>>>   
public class Calcu implements Runnable { public static int i = 0; //public volatile static int i = 0; @Override public void run() { for (int j = 0; j < 10000; j++) { i++; } } public static void main(String[] args) throws InterruptedException { Thread[] threads = new Thread[100]; for(int i = 0;i < 100;i++){ threads[i] = new Thread(new Calcu()); threads[i].start(); } for (int j = 0; j < 100; j++) { threads[j].join(); } System.out.println(i); } } 使用volatile,输出在50W左右,不使用结果在80w左右。
<无标签>
举报
cassia_
发帖于1周前 2回/124阅
共有2个答案 最后回答: 1周前
  1. 首先搞清thread 中join() 的用法。 join方法顾名思义 就是允许往线程中添加东西;join方法可以用于临时加入线程。换言之,头个线程在运算过程中,可以让另一个线程,排在它的后面,等头一个线程运行完毕,下一个线程才能开始运行。因此,如果在 以i为控制变量的for 循环体代码块的最后, 添加一行 "threads[i].join();",循环体每次运行的操作就改成:创建一个Calcu类的线程 threads[i]; 启动这个线程; 调用这个线程的join()方法,以确保这个线程(i)执行完成之后,才开始执行下一个线程。如此一来,这100个线程的操作就有了“一个一个排队挨个儿来”的正常秩序, 即确保头一个线程执行完成之后,才开始执行下一个线程。结果无论使用或不使用volatile修饰,线程都是按顺序一个一个地执行。 每个线程的贡献都是使Calcu类的这个静态变量 i增加10000, 那么 100 个线程的总贡献就是 100×10000, 100W。测试证明正是如此, 两种情况的输出都是 1000000, 一个不少 。 for(int i = 0;i < 100;i++){ threads[i] = new Thread(new Calcu()); threads[i].start(); threads[i].join(); }  
  2. 可惜楼主没有及时地 (在这个控制变量为 i 的循环体代码里) 让刚诞生的线程 threads[i],调用 join()方法,使 这100个线程排好队, 而是在第一个循环体执行完成之后,才开启第二个以 j 为控制变量的for 循环, 来调用方法 join()。晚矣!因为后一个线程 没有等到头一个执行完成就启动了。形成的局面是: 操作系统 早就 并发、 异步(非同步) 地执行了这100个线程。
  3. java虚拟机(jvm)为了提高线程读写数据的效率,对其运行时的普通变量内存分配,另有安排:每一个线程运行时都为其另建一个线程栈(stack), 就是一个读写数据更快的缓冲寄存器(cache/register),或称线程的工作内存。线程栈保存的是线程运行时变量值的信息。就你的情况而言,每个线程都从主内存堆(heap)里,依次各自取得 Calcu 类的 即时静态变量 i 的值。用这个值,在自己的工作(栈)内存建立一个i的副本。此后线程就不再和主内存的变量i有任何关系,而是直接 修改/读取 在本地工作内存的变量i的副本值。直到此线程完成方法 run() 里的for循环,再与主内存发生关系( 将此时的副本 i 的值,赋给主内存的静态变量 i)。
  4. 冠以volatile的变量则不然。关键字volatile告诉 java 虚拟机(jvm), 它所修饰的变量不保留拷贝,若要读写这个变量,必须直接访问主内存(堆)中的地址。
  5. 这里,异步线程共享一个资源(static int i )会产生"偏差". 比如,第一、第二个线程相继从主内存都提取到 0 值,因为碰巧此期间没有线程更新主内存。这两个线程各自将i增至1后,又相继以数值 1 赋给主内存 i,使主内存的 i 只更新到了 1。按道理,执行这两个线程的这段操作后,主内存的 i 应当更新 到 2,这就是所谓的偏差读写操作的量越大,发生所谓的偏差就越大。
  6. 不加 volatile 修饰 static int i, 就是 3 所述的情况,每个线程从主内存取一次值,依靠自己的快速工作内存(读写数据更快的缓冲寄存器cache/register) 完成10000次循环,再将最终结果,赋值给主内存。由于每个线程只需要以主内存发生读、写操作各一次, 且在工作内存里的读写速度较快,故损失(误差)较小,楼主会看到较大的输出值(80w左右)。
  7. 加上volatile 修饰,就是 4 所述的情况,每个线程每次只能从主内存取 i 值,使之增加 1 后,就必须将结果赋予(更新)主内存, 因为 冠以 valatile 的变量只能与主内存发生关系。这样一来,每个线程在主内存上要进行量大得多的读写操作,故损失(误差)大得多,楼主会看到较小的输出值(50w左右)。
volatile保证了 i++ 的时候,取到i的值是最新的,但是i++的取值和赋值并不是原子性操作,还是会同时写入。 不加volatile时候,i++取到的i的值可能不是最新的,也不能保证不会同时写入。 此处为保证自增为原子性操作,应使用AtomicInteger。
--- 共有 1 条评论 ---
cassia_我不是为了得到准确结果。而是为什么加了volatile后程序结果会比不加误差更大 1周前 回复
顶部