首页 > 技术文章 > volatile关键字

xiazhenbin 2020-10-23 15:21 原文

在讲解volatile关键字的时候,我们先看一个简单的例子:

package SynchronizedCass;

public class t4 {
    public static void main(String[] args) {
        try {
            RunThread thread = new RunThread();
            thread.start();
            Thread.sleep(1000);
            thread.setRunning(false);
            System.out.println("已经被赋值为false");
        } catch (InterruptedException e) {
            // TODO 自动生成的 catch 块
            e.printStackTrace();
        }
    }
}

class RunThread extends Thread    {
    private boolean isRunning = true;
    
    public boolean isRunning() {
        return isRunning;
    }
    
    public void setRunning(boolean isRunning) {
        this.isRunning = isRunning;
    }
    
    @Override
    public void run() {
        super.run();
        System.out.println("进入run方法了");
        while(isRunning == true) {
            //无限循环
        }
        System.out.println("线程被停止了!");
    }
}
进入run方法了
已经被赋值为false

 

从结果上看:System.out.println("线程被停止了!"); 这句代码从未被执行,程序进入了死循环,这是为什么呢?

启动线程后 private boolean isRunning = true; 变量存在于内存的公共堆栈及线程的私有堆栈中。在JVM被设置为-server模式时为了线程运行的效率。线程一直在私有堆栈中取得isRunning的值是true;虽然代码thread.setRunning(false)被执行了,更新的却是公共堆栈中的isRunning变量值false,线程的私有堆栈中任为true,所以线程thread就一直处于死循环的状态。

这个问题就是公共堆栈中的值和私有堆栈中的值不同步造成的。那么如何解决这个问题,这就到了我们今天要介绍的关键字volatile,当他修饰isRunning变量时,强制性地从公共堆栈中进行取值。

package SynchronizedCass;

public class t4 {
    public static void main(String[] args) {
        try {
            RunThread thread = new RunThread();
            thread.start();
            Thread.sleep(1000);
            thread.setRunning(false);
            System.out.println("已经被赋值为false");
        } catch (InterruptedException e) {
            // TODO 自动生成的 catch 块
            e.printStackTrace();
        }
    }
}

class RunThread extends Thread    {
    volatile private boolean isRunning = true;
    
    public boolean isRunning() {
        return isRunning;
    }
    
    public void setRunning(boolean isRunning) {
        this.isRunning = isRunning;
    }
    
    @Override
    public void run() {
        super.run();
        System.out.println("进入run方法了");
        while(isRunning == true) {
            //无限循环
        }
        System.out.println("线程被停止了!");
    }
}
进入run方法了
已经被赋值为false
线程被停止了!

我们看一下使用volatile关键字后发生了什么?

线程主体是不是强制地从公共内存中读取变量的值,它增加了实例变量在多个线程之间的可见性。

 

比较一下关键字volatile和synchronized,如下:

  • 关键字volatile解决的是变量在多个线程之间的可见性;而synchronized关键字解决的是多个线程之间的访问资源同步性
  • 关键字volatile只能用于修饰变量,而synchronized可以修饰方法,以及代码块。
  • 多线程访问volatile不会发生阻塞,而synchronized会发生阻塞。
  • volatile能保证数据的可见性,但是不能保证原子性;而synchronized可以保证原子性,也间接保证可见性(因为他会将私有内存和公共内存中的数据做同步)

 但是volatile关键字却有一个缺点,就是它是非原子的特性。简单看一下例子:

package SynchronizedCass;

public class t5 {
    public static void main(String[] args) {
        MyThread[] mythreadArray = new MyThread[100];
        
        for(int i = 0; i < 100; i++) {
            mythreadArray[i] = new MyThread();
        }
        
        for(int i = 0; i < 100; i++) {
            mythreadArray[i].start();
        }
    }
}

class MyThread extends Thread {
    volatile public static int count;
    private static void addCount() {
        for(int i = 0; i < 100; i++) {
            count++;    //++操作分为三步,而volatile无法保证原子性,只能解决变量读取时的可见性
        }
        System.out.println("count = " + count);
    }
    
    @Override
    public void run() {
        // TODO 自动生成的方法存根
        super.run();
        addCount();
    }
}

 

count = 4729
count = 4867
count = 5129
count = 5062
count = 5429
count = 4929
count = 5629
count = 5529
count = 5829
count = 5329
count = 5229
count = 6129
count = 6029
count = 5929
count = 5729
count = 6429
count = 6529
count = 6329
count = 6229
count = 6729
count = 6629
count = 6829
count = 6929
count = 7029
count = 7129
count = 7229
count = 7329
count = 7429
count = 7529
count = 7629
count = 7729
count = 7829
count = 7929
count = 8029
count = 8129
count = 8229
count = 8329
count = 8429
count = 8529
count = 8629
count = 8729
count = 8829
count = 8929
count = 9029
count = 9129
count = 9329
count = 9229
count = 9429
count = 9529
count = 9629
count = 9929
count = 9829
count = 9729

 

 这个最终的count值和我们预想的100×100 =10000不一样,而且在打印结果的过程中出现了不同步的现象。分析一下原因:

volatile关键字保证多线程读取共享变量时可以获取最新值使用,但是却是非原子的,当我们修改实例变量的数据,如count++,该操作步骤分解如下:

  • 从内存中读取count的值;
  • 计算count的值
  • 将count的值写入到内存中去

我们用图来看一下使用volatile时,出现非线程安全的原因。

 

  1. read和load阶段,从主存赋值变量到当前线程的工作内存中;
  2. use和assign阶段,执行代码,改变共享变量值;
  3. store和write阶段:用工作内存数据刷新主存对应变量的值;

当read和load之后,如果主存count变量发生修改,线程工作内存中的值由于已经加载,不会产生对应的变化,造成私有内存和公共内存中的变量不同步,出现了非线程安全的问题。

用volatile关键字,只能保证从主内存中加载到线程工作内存中的值是最新的,解决读取变量时的可见性问题,但是无法保证原子性,需要用synchronized加锁同步。

 

使用原子类i++进行操作

在进行i++操作的时候,可以用AtomicInteger原子类进行实现。

 

package SynchronizedCass;

import java.util.concurrent.atomic.AtomicInteger;

public class t5 {
    public static void main(String[] args) {
        AddCountThread countService = new AddCountThread();
        
        Thread t1 = new Thread(countService);
        t1.start();
        
        Thread t2 = new Thread(countService);
        t2.start();
        
        Thread t3 = new Thread(countService);
        t3.start();
        
        Thread t4 = new Thread(countService);
        t4.start();
        
        Thread t5 = new Thread(countService);
        t5.start();
        
    }
}

class AddCountThread extends Thread {
    private AtomicInteger count = new AtomicInteger();
    @Override
    public void run() {
        for(int i = 0; i < 10000; i++) {
            System.out.println(count.incrementAndGet());
        }
    }
}
49993
49994
49995
49996
49997
49998
49999
50000

count是一个原子类,其incrementAndGet是以原子方式将当前值加 1。可以保证原子性。

但是原子也并不完全安全,当方法和方法之间的调用不是原子的时候,就必须用同步解决。 

 

synchronized代码块具有可见性

synchronized包含两个特性:互斥性和可见性==========>“外练互斥,内修可见”

同步synchronized不仅可以解决一个线程看到对象处于不一致的状态,还可以保证进入同步方法或者同步代码块的每个线程,都看到由同一个所保护之前的所有的修改效果。

给一个synchronized可见性的一个讲解博客

 

 

 

 

 

 

 

 

 

 

 

 

 

推荐阅读