Java并发中正确使用volatile

作者:一粟   整理和翻译自Twitter实时搜索的PPT

前几天并发编程群里有同学对volatile的用法提出了疑问,刚好我记得Twitter有关实时搜索的这个PPT对这个问题解释的很清晰并有一个实际的应用场景,于是周末把这个问题摘录了一些和并发相关的内容如下:

并发 – 定义

悲观锁 – Pressimistic locking

  1. 一个线性在执行一个操作时持有对一个资源的独占锁。(互斥)
  2. 一般用在冲突比较可能发生的场景下

乐观锁 – Optimistic locking

  1. 尝试采用原子操作,而不需要持有锁;冲突可被检测,如果发生冲突,具有相应的重试逻辑
  2. 通常用在冲突较少发生的场景下

非阻塞算法 – Non-blocking algorithm

  1. 算法确保对线程间竞争共享资源时候,不会因为互斥而使任一线程的执行无限延迟;

无锁算法 – Lock-free algorithm

  1. 如果系统整个流程的执行是无阻塞的(系统某一部分可能被短暂阻塞),这种非阻塞算法就是无锁的。
  2. 无锁算法比传统的基于锁的算法对系统的开销更小,且更容易在多核多CPU处理器上扩展;
  3. 在实时系统中可以避免锁带来的延迟;
  4. CAS (compare and swap)或LL/SC(load linked/store conditional),以及内存屏障相关的指令经常被用在算法实现中。

无等待算法 – Wait-free algorithm

  1. 如果每个线程的执行都是无阻塞的,这种非阻塞算法就是无等待的(比无锁算法更好)

Java的并发

  1. Java的内存模型并不保证一个线程可以一直以程序执行的顺序看到另一个线程对变量的修改,除非两个线程都跨越了同一个内存屏障。(Safe publication)

Java内存模型

代码顺序规则

  1. 一个线程内的每个动作 happens-before 同一个线程内在代码顺序上在其后的所有动作

volatile变量规则

  1. 对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入

传递性

  1. 如果A happens-before B, B happens-before C,那 A happens-before C

Safe publication案例

class VolatileExample {
	int x = 0;
	volatile int b = 0;

	private void write() {
		x = 5;
		b = 1;
	}

	private void read() {
		int dummy = b;
		while (x != 5) {
		}
	}

	public static void main(String[] args) throws Exception {
		final VolatileExample example = new VolatileExample();
		Thread thread1 = new Thread(new Runnable() {
			public void run() {
				example.write();
			}
		});
		Thread thread2 = new Thread(new Runnable() {
			public void run() {
				example.read();
			}
		});
		thread1.start();
		thread2.start();
		thread1.join();
		thread2.join();
	}
}

x并不需要定义为volatile, 程序里可以有需要类似x的变量,我们只需要一个volatile变量b来确保线程a能看到线程1对x的修改:

  1. 根据代码顺序规则,线程1的x=5; happens-before b=1;; 线程2的int dummy = b; happens-before while(x!=5);
  2. 根据volatile变量规则,线程2的b=1; happens-before int dummy=b;
  3. 根据传递性,x=5; happens-before while(x!=5);

JSR-133

在JSR-133之前的旧Java内存模型中,虽然不允许volatile变量之间重排序,但旧的Java内存模型仍然会允许volatile变量与普通变量之间重排序。JSR-133则增强了volatile的内存语义:严格限制编译器(在编译器)和处理器(在运行期)对volatile变量与普通变量的重排序,确保volatile的写-读和监视器的释放-获取一样,具有相同的内存语义。

延伸阅读: JSR-133: JavaTM Memory Model and Thread Specification, The JSR-133 Cookbook for Compiler Writers

参考链接

  1. http://2011.lucene-eurocon.org/attachments/0002/8787/Busch_twitter_realtime_search_eurocon_11.pdf
  2. http://www.rossbencina.com/code/lockfree
  3. http://rethinkdb.com/blog/lock-free-vs-wait-free-concurrency/
  4. http://www.infoq.com/cn/articles/java-memory-model-4
  5. JSR-133: JavaTM Memory Model and Thread Specification
  6. The JSR-133 Cookbook for Compiler Writers

原创文章,转载请注明: 转载自并发编程网 – ifeve.com本文链接地址: Java并发中正确使用volatile

hugozhu

花名一粟,淘宝资深架构师。

Latest posts by hugozhu (see all)

FavoriteLoading添加本文到我的收藏
  • Trackback 关闭
  • 评论 (12)
    • netcomm
    • 2013/10/18 10:07上午

    “根据volatile变量规则,线程2的b=1; happens-before int dummy=b;” 这里有个小错误,应该是线程1的b=1。

    • mongo
    • 2013/12/25 8:06下午

    Safe publication案例中,代码顺序是:先启动线程1,然后线程2
    thread1.start();
    thread2.start();
    有没有可能运行的时候,线程2先运行,线程1后运行,导致
    “线程2的b=1; happens-before int dummy=b;”不成立呢?求解

    • zfox
    • 2013/12/31 10:13上午

    mongo :
    Safe publication案例中,代码顺序是:先启动线程1,然后线程2
    thread1.start();
    thread2.start();
    有没有可能运行的时候,线程2先运行,线程1后运行,导致
    “线程2的b=1; happens-before int dummy=b;”不成立呢?求解

    同问~ 这完全有可能thread2 先运行的吧

    • Greenster
    • 2014/01/24 11:15上午

    thread2是有可能在thread1之前运行的,至少在理论上如此。如果thread2先运行,还会进入死循环,因为忙等待条件x!=5中的x并不是volatile的,所以thread1修改了x的值之后,对thread2是不可见的。
    所以示例程序应该确保thread1在thread2之前运行。

    • 好细心的人,一开始还以为你没理解happens-before呢:),作者那段代码确实不能保证程序一定能退出死循环,不过,你的说法也欠妥,不应该是“确保thread1在thread2之前运行”(线程的执行先后,不能保证指令的先后,除非一个执行完了再执行另一个),而是,应该确保进入死循环前,dummy(也就是b)是等于1的,这是第一个happens-before,有了它才可以推出后面的happens-before

      • skyim
      • 2014/10/11 5:13下午

      我把这个程序执行了一下
      thread2 有可能在 thread1 之前执行的的

      但是我有1个问题,就算thread2再thread1之前执行,thread1修改了x的值,thread2是可见的

      我执行了了如下代码
      thread2 就算read先执行, thread1 的write写完的值 thread2睡眠之后还是读取到了thread1的值x

      class VolatileExample {
      int x = 0;
      volatile int b = 0;

      private void write() {
      System.out.println(“write”);
      x = 5;
      b = 1;
      }

      private void read() {
      int dummy = b;
      System.out.println(“read”);
      try {
      Thread.currentThread().sleep(1000);
      } catch (InterruptedException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
      }
      System.out.println(“sleep”);
      while (x != 5) {
      System.out.println(x);
      }
      }

      public static void main(String[] args) throws Exception {
      final VolatileExample example = new VolatileExample();
      Thread thread1 = new Thread(new Runnable() {
      public void run() {
      example.write();
      }
      });
      Thread thread2 = new Thread(new Runnable() {
      public void run() {
      example.read();
      }
      });
      thread1.start();
      thread2.start();

      thread1.join();
      thread2.join();

      }
      }

    • connecterror
    • 2015/07/10 12:19下午

    我在作者的代码上,加了一个count计数,通过观察count值,可以得知read()的循环次数,测试几次发现:不能保证 “x=5; happens-before while(x!=5);”,这两条语句的先后,更像随机的。这是作者弄错了么?还是我哪里弄错了?

    class VolatileExample {
    int x = 0;
    volatile int b = 0;
    int count = 0;

    private void write() {
    x = 5;
    b = 1;
    }

    private void read() {
    int dummy = b;
    while (x != 5) {
    count++;
    }
    System.out.println(count);
    }

    public static void main(String[] args) throws Exception {
    final VolatileExample example = new VolatileExample();
    Thread thread1 = new Thread(new Runnable() {
    public void run() {
    example.write();
    }
    });
    Thread thread2 = new Thread(new Runnable() {
    public void run() {
    example.read();
    }
    });
    thread2.start();
    thread1.start();
    thread1.join();
    thread2.join();
    }
    }

    • vincentff7
    • 2016/02/23 3:43下午

    查了下网上对volatie的解释,这是正确的吗?

    多线程环境下,线程首次使用变量,会从堆中先load一份副本到本地空间。之后如果堆中该变量发生了修改,线程不会看到。
    如果使用volatie,则保证每次访问变量,都是用堆中最新的,而不是本地空间的副本。

      • wokaole
      • 2016/02/24 5:39下午

      volatile是用来保证变量的可见性的
      如果使用了volatile,那么线程就不会把共享变量从主内存中复制一份到自己的本地空间中当做副本,每次都是通过主内存来进行访问。
      你说的是正确的

    • Ryan
    • 2016/03/25 10:24下午

    这个缺少进一步说明啊。。。

    • 232432434
    • 2017/03/23 4:41下午

    楼主,为什么我无论用android或者java程序写的,编译运行后执行的结果都是x=5,b=1呢?执行多次都是这个结果?最后结果一样的。无论有volatile修饰b或没有,都是相同的结果。我是在join后语句输出的System.out.print(b+”,” +x);

    • 232432434
    • 2017/03/23 4:43下午

    楼主,为什么我无论用android或者java程序写的,编译运行后执行的结果都是x=5,b=1呢?执行多次都是这个结果?最后结果一样的。无论有volatile修饰b或没有,都是相同的结果。我是在join后语句输出的System.out.print(b+”,” +x);网上的例子都是和我执行结果统统不同,我也不知道我相信网上的还是相信你的,还是相信我的代码了。。

您必须 登陆 后才能发表评论

return top