多线程一直以来都是面试必考点,而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相对比较简单些,它是用来对变量或代码块进行加锁,一次只能通过一个线程,其他的线程需要等待。这样是能保证变量的原子性的,因为对变量来说永远是单线程的。


















看一遍就懂,详解java多线程——volatile的更多相关文章

  1. 异常处理器详解 Java多线程异常处理机制 多线程中篇(四)

    在Thread中有异常处理器相关的方法 在ThreadGroup中也有相关的异常处理方法 示例 未检查异常 对于未检查异常,将会直接宕掉,主线程则继续运行,程序会继续运行 在主线程中能不能捕获呢? 我 ...

  2. 【Java学习笔记之三十四】超详解Java多线程基础

    前言 多线程并发编程是Java编程中重要的一块内容,也是面试重点覆盖区域,所以学好多线程并发编程对我们来说极其重要,下面跟我一起开启本次的学习之旅吧. 正文 线程与进程 1 线程:进程中负责程序执行的 ...

  3. 【java】详解java多线程

    目录结构: contents structure [+] 线程的创建与启动 继承Thread类创建线程类 实现Runnable接口创建线程类 使用Callable和Future创建线程 线程的生命周期 ...

  4. 详解Java多线程锁之synchronized

    synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法. synchronized的四种使用方式 修饰代码块:被修饰的代码块称为同步语句块,其作用的范围是大括号{}括 ...

  5. 详解Java多线程编程中LockSupport

    LockSupport是用来创建锁和其他同步类的基本线程阻塞原语. LockSupport中的park() 和 unpark() 的作用分别是阻塞线程和解除阻塞线程,而且park()和unpark() ...

  6. 详解Java多线程编程中LockSupport类的线程阻塞用法

    LockSupport类是Java6(JSR166-JUC)引入的一个类,提供了基本的线程同步原语.LockSupport实际上是调用了Unsafe类里的函数,归结到Unsafe里,只有两个函数: p ...

  7. 从缓存入门到并发编程三要素详解 Java中 volatile 、final 等关键字解析案例

    引入高速缓存概念 在计算机在执行程序时,以指令为单位来执行,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入. 由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这 ...

  8. java基础(十五)----- Java 最全异常详解 ——Java高级开发必须懂的

    本文将详解java中的异常和异常处理机制 异常简介 什么是异常? 程序运行时,发生的不被期望的事件,它阻止了程序按照程序员的预期正常执行,这就是异常. Java异常的分类和类结构图 1.Java中的所 ...

  9. 详解Java中的clone方法

    详解Java中的clone方法 参考:http://blog.csdn.net/zhangjg_blog/article/details/18369201/ 所谓的复制对象,首先要分配一个和源对象同样 ...

随机推荐

  1. Kafka架构

    一.Kafka介绍 Kafka是Linkin在2010年开源的分布式发布订阅消息系统,Kafka是高吞吐量的消息订阅系统. 二.Kafka结构 Kafka由三部分构成,producer.broker. ...

  2. Activiti工作流与spring集成

    一.前言 前面Activiti工作流的学习,说明了Activiti的基本应用,在我们开发中可以根据实际的业务参考Activiti的API去更好的理解以及巩固.我们实际的开发中我们基本上都使用sprin ...

  3. 使用淘宝的npm代理下载模块

    npm install node-sass --registry=http://registry.npm.taobao.org

  4. Maven打包命令

    mvn clean 会把原来target目录给删掉重新生成.mvn install 安装当前工程的输出文件到本地仓库,然后打包mvn clean install 先删除target文件夹 ,然后打包到 ...

  5. Python基础笔记系列十四:python无缝调用c程序

    本系列教程供个人学习笔记使用,如果您要浏览可能需要其它编程语言基础(如C语言),why?因为我写得烂啊,只有我自己看得懂!! python语言可以对c程序代码进行调用,以弥补python语言低性能的缺 ...

  6. hiho 有序01字符串 dp

    题目1 : 有序01字符串 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 对于一个01字符串,你每次可以将一个0修改成1,或者将一个1修改成0.那么,你最少需要修改多少 ...

  7. 通栏banner自适应各个设备

    思路:图片不要设置为div的background,因为你设置了background-size,但是div的height没设置一样没用,除非你搞个js判断,动态刷新. 换一种思路,直接用<img& ...

  8. 转载:10+ handy and reusable jQuery code snippets

    源地址:http://www.catswhocode.com/blog/10-handy-and-reusable-jquery-code-snippets Smooth scrolling to t ...

  9. bzo1016: [JSOI2008]最小生成树计数

    现在给出了一个简单无向加权图.你不满足于求出这个图的最小生成树,而希望知道这个图中有多少个不同的最小生成树.(如果两颗最小生成树中至少有一条边不同,则这两个最小生成树就是不同的).由于不同的最小生成树 ...

  10. Python之路,Day8 - 面向对象编程进阶

    本节内容: 面向对象高级语法部分 经典类vs新式类 静态方法.类方法.属性方法 类的特殊方法 反射 异常处理 Socket开发基础 作业:开发一个支持多用户在线的FTP程序 面向对象高级语法部分 经典 ...