看一遍就懂,详解java多线程——volatile

来源:互联网 发布:停车位软件哪个好 编辑:程序博客网 时间:2024/05/22 02:23

多线程一直以来都是面试必考点,而volatile、synchronized也是必问点,这里我试图用容易理解的方式来解释一下volatile。

来看一下它的最大特点和作用:

一 使变量在多个线程间可见

猛一看很奇怪,我定义个变量就好了,大家都能访问啊,为毛在多个线程间会有变量不可见?
换种说法,我在一个线程里去修改另外一个线程的变量,可能会修改不成功!而且是永远不成功。
这下更懵逼了,为毛?
来看一下java的内存模型简易图


这个图我来解释一下,先看堆内存区域(被所有线程共享)这个地方。
首先我们搞明白堆里放什么,然后搞明白哪些地方的内存的值是被所有线程共享的。
堆里放的是对象本身,还有数组,不放基本类型(局部变量)和对象的引用(指针)。譬如Person p = new Person();这一下之后,如果Person类里有多个属性int age、double weight、String name什么的,那么在堆里会为Person分配一块内存来装下它所有的属性,然后用一个指针指向这块内存地址,就是p,p会放到栈里去。通过p这么一个指针,你就可以找到Person这么一大块内存了。注意,前面说了堆里不放基本类型是指方法里的局部变量,而类的属性是全局变量。这一块内存是被所有线程共享的。
还有个内存区域也是所有线程共享的,就是方法区。方法区放的是static变量(全局唯一一份)和class类信息(类名、包名、方法名、修饰符public等等),还存在java中很特殊的东西——String,也就是所谓的String常量池。
资料:
http://www.cnblogs.com/wangguoning/p/6109377.html;
http://www.cnblogs.com/whgw/archive/2011/09/29/2194997.html
http://blog.csdn.net/shang02/article/details/51966939
http://www.cnblogs.com/xiohao/p/4278173.html
这些变量是所有线程共享的,so what?
这下就用的上上面那个图了。我们所谓的多线程问题,线程不安全之类的指的就是同一个变量被多个线程同时操作时会发生数据不同步的情况。如果只有一份数据,大家操作的都是同一个值,怎么会不同步呢,为毛呢?
很简单,因为操作的不是同一个数据。
1 每个线程都有一个自己的本地内存空间,线程执行时,先从共享内存区域中取到共享变量,然后干自己的事
2 事毕,裤子穿好,然后在某个时间再把变量刷新回主内存
看到了吧,这是有时间差的,你从主内存里取到的值不见得什么时候被替换了,这样就不同步了,你可能不小心操作的就不是本人而是它双胞胎妹妹了。单线程因为只有一个线程去修改,所以没问题。
而每个线程所维护的这个共享变量的副本可是不开放的,只有自己可见。
证明一下:
public class OneThread extends Thread {    private boolean running = true;    @Override    public void run() {        System.out.println("进入run方法");        while (running) {        }        System.out.println("线程执行完毕");    }    public void setRunning(boolean running) {        this.running = running;    }}

测试类
public class Test {    public static void main(String[] args) throws InterruptedException {        OneThread oneThread = new OneThread();        oneThread.start();        Thread.sleep(1000);        oneThread.setRunning(false);    }}
在OneThread类中有一个全局变量running,它会进到堆里,被所有线程共享。
Test类中,main是主线程,这样就有两个线程去操作running这个变量。
倘若running是唯一的一份,所有的线程都操作的是同一个running变量,那么当在main中setRunning false后,OneThread就会退出死循环并打印“线程执行完毕”。
我们运行Test


然而,它死循环了……running并没有被修改。
这里要提一下JVM -server模式和JVM -client方式,看这篇讲区别http://blog.csdn.net/zhuyijian135757/article/details/38391785
我的是64位的java,通过java -version确认是JVM server模式,64位只支持server VM。

通过上面的JVM内存模型的图可知,当main线程试图访问running变量时,会先从主内存复制一份到自己的线程内存,修改为false后再刷新回主内存。
刷新回去是没问题,问题是JVM在server模式下,线程会一直在私有内存空间中去读取running变量,也就是说OneThread线程它一直读的是自己复制出来的running,它不会再去读主内存被修改过的running了。这就是问题所在。
为了证明running已经被main修改成功了,我们再加一个线程来看看running的值
public class Test {    public static void main(String[] args) throws InterruptedException {        OneThread oneThread = new OneThread();        oneThread.start();        Thread.sleep(1000);        oneThread.setRunning(false);        new Thread(() -> System.out.println(oneThread.isRunning())).start();    }}
这里我们再起一个线程去读取running的值,这时读取的就是主内存的值了。



可以看到false已经打印了,但是死循环还在进行中。说明,OneThread自打复制了running的值到自己的线程空间后,就没再改过了,一直死循环。
那么,我们可以说,线程间的变量是不可见的。
这个问题怎么解决呢?是不是有人想说,static,static不是独一份吗,那么可以去试一下将running变成public static。
结果发现然并卵,static也阻止不了这个死循环。为毛?还是最上面的JVM的图,里面说过了,堆里的和方法区里的都是多线程共享,static是在方法区的,和堆里的效果没区别。
这下怎么办,OneThread根本不认外界的修改,其实也不是了,是因为这个例子比较特殊,是个死循环,我们稍微修改一下
public class OneThread extends Thread {    private boolean running = true;    @Override    public void run() {        System.out.println("进入run方法");        try {            Thread.sleep(1100);            System.out.println(running);        } catch (InterruptedException e) {            e.printStackTrace();        }        System.out.println("线程执行完毕");    }    public void setRunning(boolean running) {        this.running = running;    }    public boolean isRunning() {        return running;    }}


我们不让OneThread死循环,让它睡的时间稍微比main多一点,运行看看。
哎呦喂,修改成功了,OneThread终于认它亲生的私生子了。
实际上,大部分场景下,我们都是在用多线程进行不安全的操作,好像并没有出问题,不是它没问题,而是场景不够极端而已。
终于要引到本篇的主角volatile了!
volatile的作用是强制线程从主内存读取volatile修饰的变量。
也可以称之为共享变量被修改后,会迅速通知所有的线程。

我们用volatile修饰running后,再试一下那个死循环代码,终于OK了。
http://www.cnblogs.com/tf-Y/p/5266710.html 这一篇用别的意思来解释了一下。

二 volatile是非原子性的

从上面我们知道,volatile修饰的变量能迅速通知其他线程,避免读取到的值是不新的。《编程思想》上说过,使用volatile时,我们能获得简单的set、get操作的原子性。
也就是像上面的例子那样,简单的set、get是能保证最新的。
but,在其他操作下,volatile并不是原子性的,譬如n++,n--这类的操作。为什么呢?
n++可不是一步,它其实是3步
1 从内存中读取变量n的值
2 将n的值加1
3 将加1后的值写回内存
这就是问题所在,n++在某段时间内并不是独享n的,volatile修饰后,其他线程也能修改n,你刚走完第一步n=1,将1读到了线程自己的内存空间里,第二步时准备把n加1呢,却被别人捷足先登,先操作了n,把它变成了10,然后你才做的加1,此时n已经是11了。同样的,第二步到第三步也是会出问题的,因为volatile会迅速刷新所有线程对n的修改,在被修改的空隙内,你不独享这个变量。volatile只能保证你每次读的时候,是从主内存读的,但保证不了你读之后对它操作然后再写回到主内存这段过程中它的值的不确定性。
import java.util.concurrent.BrokenBarrierException;import java.util.concurrent.CyclicBarrier;public class VolatileTest {    private static CyclicBarrier barrier = new CyclicBarrier(100);    private static volatile int count;    public static void main(String[] args) {        MyThread[] mythreadArray = new MyThread[1000];        for (int i = 0; i < 1000; i++) {            mythreadArray[i] = new MyThread();            mythreadArray[i].start();        }    }    static class MyThread extends Thread {        private void addCount() {            for (int i = 0; i < 100; i++) {                count++;            }            System.out.println("count=" + count);        }        @Override        public void run() {            try {                barrier.await();                addCount();            } catch (InterruptedException e) {                e.printStackTrace();            } catch (BrokenBarrierException e) {                e.printStackTrace();            }        }    }}


这个例子是起1000个线程,对同一个static的count值进行加100操作。



如果是单线程情况下,打印的count应该是100的整数倍,在多线程下情况就有变化了,每次运行结果都不一样,可以看到count的值比较随心所欲。
这里展示的是多线程下volatile并不能保证在各个线程都是最新的、原子的。其实如果想要自增的原子性的话,java提供了一个AutoicInteger类。

三 和Synchronize对比

从上面的例子可以得到结论,volatile用于set、get时多线程能及时感知变量的修改,每次去get时都是从主内存中读取的最新值。
synchronize相对比较简单些,它是用来对变量或代码块进行加锁,一次只能通过一个线程,其他的线程需要等待。这样是能保证变量的原子性的,因为对变量来说永远是单线程的。