初识volatile

有时候仅仅为了一个或者两个的实例域就是用synchronized的话,开销就会很大,而Java为我们提供了另一种同步的免锁机制,volatile。Volatile可以看成是synchronized的轻量级,功能也仅仅是synchronized的一部分,认识volatile之前,先认识Java内存模型和Java的原子性,可见性,有序性

Java的内存模型

在Java虚拟机中,每一个线程私有的就是JVMstack ,本地方法栈,PC,而共享的就是Java堆和方法区(包含常量池)。Java创建的对象都在堆中分配,所有的实例域,静态变量,数组的元素都在堆中,都是被线程共享的。

而Java线程之间的通信是通过共享内存来实现的,也就是隐式通信,而线程通信是由Java内存模型控制的(JMM),JMM决定着一个线程对一个共享变量的写入修改何时对另一个线程可见(也就是另一个线程得到那个最新的修改值),JMM定义了线程与主存之间的抽象关系:线程之间的共享变量存储在主存中,每个线程在自己的栈中(栈中有栈帧)都有一个私有的本地内存空间,其他线程不可访问,本地内存保存着这个共享变量的拷贝。线程不能直接对共享变量直接写,先写入本地内存,然后由JMM决定什么时候将本地的值回写打主存中。

本地内存是JMM的一个抽象概念,并不真实存在,抽象示意图:


先看个例子:

public class Main {

   
public staticvoid main(String[] args) {
        Test test =
new Test(5);

        
MyThread myThread =new MyThread(test);
       
MyRunnablemyRunnable = new MyRunnable(test);

       
Thread thread = new Thread(myRunnable);

       
myThread.setName("MyThread");
       
thread.setName("MyRunnable");
       
thread.start();
        
myThread.start();
   
}
}

public class Test {

   
public int test;

    public
Test(int test) {
       
this.test = test;
   
}
}

public class MyThread extends Thread {
    Test
test;

    public
MyThread(Test test) {
       
this.test = test;
   
}

    
@Override
   
public void run() {
        System.
out.println(Thread.currentThread().getName()+" " +test.test);
       
test.test = 10;
        try
{
            System.
out.println(Thread.currentThread().getName()+" test.test= 10;");
           
System.out.println(Thread.currentThread().getName()+" 睡觉");
           
Thread.currentThread().sleep(2000);
           
System.out.println(Thread.currentThread().getName()+" " +test.test);
       
} catch (InterruptedException e) {
            e.printStackTrace()
;
       
}
        System.
out.println(Thread.currentThread().getName()+" " +test.test);
   
}
}

public class MyRunnable implements Runnable {

    Test
test;

    public
MyRunnable(Test test) {
       
this.test = test;
   
}

   
@Override
   
public void run() {

        System.
out.println(Thread.currentThread().getName()+" " +test.test);
       
test.test = 20;
//        try {
           
System.out.println(Thread.currentThread().getName()+" test.test= 20;");
 
System.out.println(Thread.currentThread().getName()+""+test.test);
   
}
}

输出:(这个输出随机性很大,运行很多次才偶然得到)

MyRunnable 5

MyRunnable test.test = 20;

MyRunnable 20

MyThread 5

MyThread test.test = 10;

MyThread 睡觉

MyThread 10

MyThread 10

结果说明:在main线程创建了线程MyThread和MyRunnable两个线程,实例test的域原始值是5。由输出可以知道,MyRunnable线程先执行完之后MyThread线程才开始执行,可以看到,在MyRunnable线程中,我们把test对象的域改为了20,但是在MyThread线程中打印出来的值还是5,要么是MyRunnable的值没写回内存,要么就是MyThread的本地值是一开始test域没改之前的值(没更新MyRunnable改过的值)。这就很好的印证了上面那幅图

原子性

如果学过操作系统或者数据库都会有这样的概念,在生活中,A转100给B执行的语句就是

         A = A -100;         

         B= B+100;

那么在数据库写入的时候就会用事务管理来操作,保证其原子操作,也就是害怕执行了         A = A -100;         

突然故障导致不能往下执行,那么A就会无端端少了100,而B也没有得到这100,原子操作就是要么执行完全部,要么全部不执行,在数据库中,事务管理器针对上面的情况就会恢复A的值,也就是让A重新+100,恢复以前的值。Java的原子性也就是差不多的意思。

例子:

①X = 100;

②Y=X +100;

①  就是原子操作,对基本数据类型的写入和读取都是原子性操作,即这些操作要么不执行,要么执行,不能被打断

②  而就不是原子操作了,因为涉及到X的读取,X+100后赋值给Y,虽然这两个分开就是原子操作,但是结合一起就不是原子操作了,这两个过程可能被打断

原子就是不能再分的最小的例子,所以对应操作也是不能再分和打断

java.util.concurrent.atomic包含AtomicBoolean,AtomicLong和AtomicReference这些原子类仅供开发并发工具的系统程序员使用,应用程序员不应该使用这些类。

可见性

其实可见性在上面的代码例子中已经可以体现,意思就是线程1修改了共享变量的值之后,其他的线程可以知道这个变量值已经修改,并且能够得到这个新值

有序性

在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性,更多的了解可以看这里

重排序并不是Java特有的,在操作系统也是会有的,为了性能优化而重排序是很正常的

Volatile关键字

当一个变量定义为volatile之后,它具备两项特性

①可见性

②禁止指令重排序优化

可见性demo:

public class Main {

    public static void main(String[] args) throws InterruptedException {
        MyThread myThread = new MyThread();

        myThread.start();
        Thread.currentThread().sleep(1000);
        System.out.println("sleep 完成");
        myThread.setFlag(true);

    }}
public class MyThread extends Thread {

    boolean flag = false;

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
    @Override
    public void run() {
        while (!flag) {
        }
    }
}

结果:

结果说明:

每个线程都有自己的工作(本地内存)内存,myThread线程读取了flag的值到自己的工作内存中,拷贝了一份,然后一直进入死循环,而当main线程也有自己的工作内存,它拿到了flag的值,改变了flag的值,但是或许还没来得及将它写会到住内存中就已经结束了,而myThread一直不知道flag的值已经改变,所以一直循环下去

加上volatile关键字

volatile boolean flag = false;

①  使用volatile修饰的变量,当变量改变之后,会立马强制写会内存

②  当某个线程修改了这个变量,其他线程使用前,会直接从内存中读取一次

 Volatile不具有原子性(一般)

public class Main {

    volatile static int flag = 0;
    public static void increment() {
        flag++;
    }

    public static void main(String[] args) throws InterruptedException {

        final Main test = new Main();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increment();
                };
            }.start();
        }
        //保证前面的线程都执行完
        while(Thread.activeCount()>2)
            Thread.yield();
        System.out.println(test.flag);

    }
}

这里是jdk1.8所以while(Thread.activeCount()>2)如果还是1.7的话就改为大于1

输出的结果大多数情况下都是小于10000,因为自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存。那么就是说自增操作的三个子操作可能会分割开执行。

假如某个时刻变量inc的值为10,线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了;然后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量inc的缓存行无效,所以线程2会直接去主存读取inc的值,发现inc的值时10,然后进行加1操作,并把11写入工作内存,最后写入主存。然后线程1接着进行加1操作,由于已经读取了inc的值(不会再次去主存读数据),注意此时在线程1的工作内存中inc的值仍然为10,所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存。那么两个线程分别进行了一次自增操作后,inc只增加了1。

自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的。

正确使用 volatile 变量的条件

您只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:

·       对变量的写操作不依赖于当前值。

·       该变量没有包含在具有其他变量的不变式中。

实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。

第一个条件的限制使 volatile 变量不能用作线程安全计数器。虽然增量操作(x++)看上去类似一个单独操作,实际上它是一个由读取-修改-写入操作序列组成的组合操作,必须以原子方式执行,而 volatile 不能提供必须的原子特性。实现正确的操作需要使 x 的值在操作期间保持不变,而 volatile 变量无法实现这点。(然而,如果将值调整为只从单个线程写入,那么可以忽略第一个条件。)

大多数编程情形都会与这两个条件的其中之一冲突,使得 volatile 变量不能像 synchronized 那样普遍适用于实现线程安全。清单 1 显示了一个非线程安全的数值范围类。它包含了一个不变式 —— 下界总是小于或等于上界。

参考:

http://www.jianshu.com/p/4377b3245a2c

http://www.ibm.com/developerworks/cn/java/j-jtp06197.html

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值