线程基础知识系列(五)认识volatile

本篇文章主要讨论的关键字是volatile.

  1. volatile使用场景

  2. volatile介绍

  3. volatile vs synchronized vs lock

1 volatile使用场景

Volatile的官方定义

Java语言规范第三版中对volatile的定义如下: java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁更加方便。如果一个字段被声明成volatile,java线程内存模型确保所有线程看到这个变量的值是一致的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.threadexample.sysn;
public class ViolatileTest extends Thread {
    boolean keepRunning = true;
    public void run() {
        while (keepRunning) {
            //这儿不能进行打印操作,否则会影响演示
          //System.out.println("runing--");
        }
 
        System.out.println("Thread terminated.");
    }
 
    public static void main(String[] args) throws InterruptedException {
        ViolatileTest t = new ViolatileTest();
        t.start();
        Thread.sleep(1000);
        t.keepRunning = false;
        System.out.println("keepRunning set to false.");
    }
}

这个例子,想达到的效果很简单,main线程休眠1秒,关闭t线程。可运行结果演示,线程t并没有按预期正常退出。

如果使用volatile关键字修饰keepRunning 关键字,可达到预期效果。为什么呢?

2.volatile介绍

volatile的中文意思是“不稳定,反复无常的”。但是,在java领域,被翻译成“可见性”。

在java中,当volatile用于一个作用域时,java保证如下:

2.1 保证有序性

java内存模型,支持指令的重排序。为什么重排序,主要是从性能优化层面考虑的。主要的重排序包括以下:

编译器优化重排序
指令级并行重排序
内存系统重排序

重排序的结果:不会改变执行结果;从多线程角度看,其他线程执行顺序是无序的,从单线程角度看,本线程内的执行顺序是有序的。volatile限制了重排序。volatile的读和写建立了一个happens-before关系,类似于申请和释放一个互斥锁,与互斥锁的不同点是:不能像锁一样保证原子性访问。

2.2 保证可见性

根据JMM规定,每个工作线程分别持有本地缓存。线程之间的信息通信,均需要线程的本地缓存与共享缓存进行同步。

volatile声明的变量,不需要保存到本地缓存,也就是说每个线程访问一个volatile作用域时会在继续执行之前读取它的当前值,而不是(可能)使用一个缓存的值。

3.volatile vs synchronized vs lock

volatile与synchronized关键字,在功能上有些类似,但也有很多的不同。我个人比较喜欢通过对比的方式来学习,容易加深理解。

上面的演示例子,不使用volatile关键字也是可以的,可以使用同步synchronized。

3.1 使用synchronized,实现定时关闭线程效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.threadexample.sysn;
public class ViolatileTest extends Thread {
    private boolean keepRunning = true;
    public void run() {
        while (keepRunning) {
            System.out.println("runing--");
        }
 
        System.out.println("Thread terminated.");
    }
 
    public synchronized void setRunning(boolean keepRunning){
        this.keepRunning=keepRunning;
    }
 
    public static void main(String[] args) throws InterruptedException {
        ViolatileTest t = new ViolatileTest();
        t.start();
        Thread.sleep(1000);
        t.setRunning(false);
//        t.keepRunning = false;
        System.out.println("keepRunning set to false.");
    }
}


方面
volatile synchronized or lock
线程安全保障
仅仅保证可见性
可见性+原子性
相对并发性

独占锁,相对低

使用场景

1.对变量的写入操作,不依赖变量的当前值

2.该变量不会与其他状态变量一起纳入不变性条件中

3.访问变量时,不需要加锁


访问共享可变变量时

尽量考虑使用并发容器


1.使用violatile保证线程安全例子,这种方式是错误的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.threadexample.sysn;
import java.util.concurrent.TimeUnit;
public class CountTask implements Runnable {
    private volatile  int count;
    public void run() {
        for(int i=0;i<100;i++){
            count++;
            try {
                TimeUnit.MILLISECONDS.sleep(10);
            catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public int getCount(){return  this.count;}
    public static void main(String[] args) throws InterruptedException {
        CountTask ct = new CountTask();
        Thread t1 = new Thread(ct);
        Thread t2 = new Thread(ct);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(ct.getCount());
    }
}

这种方式不是线程安全的,因为存在状态依赖。递增操作,依赖于上一个值。




本文转自 randy_shandong 51CTO博客,原文链接:http://blog.51cto.com/dba10g/1795257,如需转载请自行联系原作者

上一篇:C# 常见错误处理(收藏)


下一篇:动态规划法(六)鸡蛋掉落问题(一)(egg dropping problem)