学习互联网架构第四课(volatile关键字)

来源:互联网 发布:大学生程序员怎么赚钱 编辑:程序博客网 时间:2024/06/07 02:55

       volatile概念:volatile关键字的主要作用是使变量在多个线程间可见。

       在说volatile关键字之前,先来看两个小例子

package com.internet.thread;public class RunThread extends Thread{    private int num = 0;    public void setNum(int num){    System.out.println(this.num);    this.num = num;    }    public void run(){    System.out.println(num);    }    public static void main(String[] args){        RunThread t1 = new RunThread();    t1.setNum(10);    t1.start();    try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}        RunThread t2 = new RunThread();        t2.setNum(20);        t2.start();    }        }
      运行结果如下,可以看到,两个线程操作的num完全没有关系,各自操作各自的。

010020
       假如我们在num前面加上static修饰

private static int num = 0;
       下面再运行main方法,结果如下,说明这时两个线程操作的是同一个变量num。

0101020
        但是多个线程同时访问同一个变量a的时候,就会出现线程问题,如下图所示。针对线程问题,我们可以采取给变量a加synchronized锁,这样无论多少个线程访问变量a都要一个一个的来,其中一个线程操作a的期间其它线程不能操作变量a,但是这样有个很大的问题就是并发太低。


         

             下面我们再来看个例子,代码如下

package com.internet.thread;public class VolatileThread extends Thread{    private boolean isRunning  = true;    private void setRunning(boolean isRunning){    this.isRunning = isRunning;    }        public void run(){    System.out.println("进入run方法..");    while(isRunning == true){    //..    }    System.out.println("线程停止");    }        public static void main(String[] args) throws InterruptedException{    VolatileThread vt = new VolatileThread();    vt.start();    Thread.sleep(3000);    vt.setRunning(false);    System.out.println("isRunning的值已经被设置了false");    Thread.sleep(1000);    System.out.println(vt.isRunning);    }}
           运行结果如下图所示,可以看到,虽然isRunning变量的值变成了false,但是while循环依然在执行,如下图所示。这显然不合理的。

        那么,为什么我们把变量值isRunning变成false而while循环却不停止呢?这其实是JDK的设计造成的,如下图所示,JDK在设计线程的时候引入了线程工作内存的机制,变量在主内存中有一份isRunning变量,在线程工作内存中存了该变量的一个副本,线程在执行的时候判断isRunning变量值的时候是从线程工作内存中去获取的,当我们在主线程中设置isRunning的值为false时,主内存中的isRunning变量的值已经变成false了,但是线程工作内存中的isRunning副本的值还是true,因此我们才会看到while循环还在一直运行的原因。JDK这样做的目的是为了避免每次获取变量值都要去主内存获取,因为这样比较消耗性能。


        那么,我们应该怎样解决这个问题呢?其实方案很简单,就是给isRunning加上volatile关键字修饰,然后重新运行main方法,这次发现while循环结束了。这才是正常的运行结果。

          这时工作机制如下图所示。可以看到,当变量被volatile关键字修饰后,线程执行引擎就会去主内存中去读取变量值,同时主内存会把改变的变量值更新到线程工作内存当中。


         用volatile关键字修饰变量虽然可以让变量在多个线程间可见,但是它并不具有原子性,我们来看下面一个例子,定义了一个addCount方法,调用一次count就加1000,如果count具有原子性的话,最后的结果应该是10000。

package com.internet.thread;public class VolatileNoAtomic extends Thread{    private static volatile int count;    private static void addCount(){    for(int i=0;i<1000;i++){    count++;    }    System.out.println(count);    }        public void run(){    addCount();    }        public static void main(String[] args){    VolatileNoAtomic[] arr = new VolatileNoAtomic[10];    for(int i=0;i<10;i++){    arr[i] = new VolatileNoAtomic();    }    for(int i=0;i<10;i++){    arr[i].start();    }    }}
          我们运行上面的代码,结果如下,可以看到最后的结果是8839,并不是我们期望的10000,从而可以得出结论:用volatile关键字修饰的变量并不具有原子性。

2000400030002000500062406763683978398839
        那么,怎样才能让变量count具有原子性呢?我们可以使用AtomicInteger,如下图所示。


        修改后,我们再运行下main方法,结果如下,虽然中间的过程不具有原子性,但是最终的结果一定是具有原子性的,这样做的好处是多个线程可以同时执行,中间过程可能有短暂的数据不一致,但是最终的结果一定是正确的。这样的例子也很常见,比如我们双11抢购商品,这么大的并发量,要说一下子就把所有数据都准确的统计出来是不可能的,因为并发量太大了,根本来不及统计,于是退而求其次,允许短暂的数据不一致,但是最终一定要做到数据准确、一致。

10002000416550004724629670008903900010000
        volatile关键字虽然拥有多个线程之间的可见性,但是却不具备同步性(也就是原子性),可以算上是一个轻量级的synchronized,性能要比synchronized强很多,不会造成阻塞(在很多开源的架构里,比如netty的底层代码就大量使用volatile,可见netty性能一定是非常不错的。)这里需要注意:一般volatile用于只针对于多个线程可见的变量操作,并不能代替synchronized的同步功能。实现原子性建议使用atomic类的系列对象,支持原子性操作(注意atomic类只保证本身方法原子性,并不保证多次操作的原子性)

       下面我们便来举个例子来说明atomic类不保证多次操作原子性,代码如下(注意此时multiAdd方法前是没有synchronized修饰的)

package com.internet.thread;import java.util.ArrayList;import java.util.List;import java.util.concurrent.atomic.AtomicInteger;public class AtomicUse {    private static AtomicInteger count = new AtomicInteger(0);    //多个addAndGet在一个方法内是非原子性的,需要加synchronized进行修饰,保证4个    //addAndGet整体原子性    public  int multiAdd(){    try {Thread.sleep(100);} catch (Exception e) {e.printStackTrace();}    count.addAndGet(1);    count.addAndGet(2);    count.addAndGet(3);    count.addAndGet(4);//1+2+3+4=10,也就是说,执行一次multiAdd方法,count就加10    return count.get();    }        public static void main(String[] args){    final AtomicUse au = new AtomicUse();    List<Thread> ts = new ArrayList<Thread>();    for(int i=0;i<100;i++){    ts.add(new Thread(new Runnable() {@Overridepublic void run() {System.out.println(au.multiAdd());}}));    }    for(Thread t:ts){    t.start();    }    }}
        我们运行main方法,结果如下所示,如果multiAdd具有原子性的话,那么应该是整10的增加,但是我们看到中间出现了诸如223、231这样的数字,说明atomic类确实不能保证多次操作的原子性(如果只写一个addAndGet方法的话,是支持原子性的,现在是4个,因此不支持方法的原子性了)。不过,虽然不能保证multiAdd方法的原子性,但是最终的结果是正确的,那就是1000,无论运行多少次,一定有1000,这说明最终是正确的。

1020304060607090801001101301201401501601701802002102002232502312402603002902802703103403303213503603803703904004104304204404504605205105004964704805305405515605705966106306065926506406206707006906806707407807607707507307307318008108008308308708708708708908909109009509509509509601000970990980
       如果我们要保证multiAdd方法的原子性的话,我们就给multiAdd方法添加synchronized关键字,如下图所示。


          我们再运行main方法,运行结果如下(由于运行结果太长,我只截取了最后面一段),可以看到数字count确实是整10的增加的,直到1000。

8308408508608708808909009109209309409509609709809901000
         volatile关键字我们就学习到这里。