Java多线程编程核心技术(二)volatile关键字

3.volatile关键字

关键字volatile的主要作用是使变量在多个线程间可见。

3.1 关键字volatile与死循环

如果不是在多继承的情况下,使用继承Thread类和实现Runnable接口在取得程序运行的结果上并没有多大的区别。如果一旦出现”多继承“的情况,则用实现Runable接口的方式来处理多线程的问题就是很有必要的。

public class PrintString implements Runnable{
    private boolean isContinuePrint = true;

    @Override
    public void run() {
        while (isContinuePrint){
            System.out.println("Thread: "+Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public boolean isContinuePrint() {
        return isContinuePrint;
    }

    public void setContinuePrint(boolean continuePrint) {
        isContinuePrint = continuePrint;
    }

    public static void main(String[] args) throws InterruptedException {
        PrintString printString = new PrintString();
        Thread thread = new Thread(printString,"Thread-A");
        thread.start();
        Thread.sleep(100);
        System.out.println("我要停止它!" + Thread.currentThread().getName());
        printString.setContinuePrint(false);
    }
}

运行结果:

Thread: Thread-A

我要停止它!main

上面的代码运行起来没毛病,但是一旦运行在 -server服务器模式中64bit的JVM上时,会出现死循环。解决的办法是使用volatile关键字。

关键字volatile的作用是强制从公共堆栈中取得变量的值,而不是从线程私有数据栈中取得变量的值。

3.2 解决异步死循环

在研究volatile关键字之前先来做一个测试用例,代码如下:

public class PrintString implements Runnable{
    private boolean isRunnning = true;

    @Override
    public void run() {
        System.out.println("Thread begin: "+Thread.currentThread().getName());
        while (isRunnning == true){
        }
        System.out.println("Thread end: "+Thread.currentThread().getName());
    }

    public boolean isRunnning() {
        return isRunnning;
    }

    public void setRunnning(boolean runnning) {
        isRunnning = runnning;
    }

    public static void main(String[] args) throws InterruptedException {
        PrintString printString = new PrintString();
        Thread thread = new Thread(printString,"Thread-A");
        thread.start();
        Thread.sleep(1000);
        printString.setRunnning(false);
        System.out.println("我要停止它!" + Thread.currentThread().getName());
    }

}

JVM有Client和Server两种模式,我们可以通过运行:java -version来查看jvm默认工作在什么模式。我们在IDE中把JVM设置为在Server服务器的环境中,具体操作只需配置运行参数为 -server。然后启动程序,打印结果:

Thread begin: Thread-A

 
我要停止它!main

代码 System.out.println("Thread end: "+Thread.currentThread().getName());从未被执行。

是什么样的原因造成将JVM设置为-server就出现死循环呢?

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

Java多线程编程核心技术(二)volatile关键字

这个问题其实就是私有堆栈中的值和公共堆栈中的值不同步造成的。解决这样的问题就要使用volatile关键字了,它主要的作用就是当线程访问isRunning这个变量时,强制性从公共堆栈中进行取值。

将代码更改如下:

volatile private boolean isRunnning = true;

再次运行:

Thread begin: Thread-A

我要停止它!main
Thread end: Thread-A

通过使用volatile关键字,强制的从公共内存中读取变量的值,内存结构如图所示:
Java多线程编程核心技术(二)volatile关键字

使用volatile关键字增加了实例变量在多个线程之间的可见性。但volatile关键字最致命的缺点是不支持原子性。

下面将关键字synchronized和volatile进行一下比较:

关键字volatile是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好,并且volatile只能修饰于变量,而synchronized可以修饰方法,以及代码块。随着JDK新版本的发布,synchronized关键字在执行效率上得到很大提升,在开发中使用synchronized关键字的比率还是比较大的。

多线程访问volatile不会发生阻塞,而synchronized会出现阻塞。

volatile能保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公共内存中的数据做同步。

再次重申一下,关键字volatile解决的是变量在多个线程之间的可见性;而synchronized关键字解决的是多个线程之间访问资源的同步性。

线程安全包含原子性和可见性两个方面,Java的同步机制都是围绕这两个方面来确保线程安全的。

3.3 volatile非原子性的特征

关键字虽然增加了实例变量在多个线程之间的可见性,但它却不具备同步性,那么也就不具备原子性。

示例代码:

public class MyThread extends Thread {
    volatile private static int count;
    @Override
    public void run() {
        addCount();
    }

    private void addCount() {
        for (int i = 0;i<100;i++){
            count++;
        }
        System.out.println(count);
    }

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

运行结果:

...
8253
8353
8153
8053
7875
7675

在addCount方法上加入synchronized同步关键字与static关键字,达到同步的效果。

再次运行结果:

....
9600
9700
9800
9900
10000

关键字volatile提示线程每次从共享内存中读取变量,而不是从私有内存中读取,这样就保证了同步数据的可见性。但在这里需要注意的是:如果修改实例变量中的数据,比如i++,也就是比

i=i+1,则这样的操作其实并不是一个原子操作,也就是非线程安全。表达式i++的操作步骤分解为下面三步:

从内存中取i的值;

计算i的值;

将i值写入到内存中。

假如在第二步计算i值的时候,另外一个线程也修改i的值,那么这个时候就会脏数据。解决的方法其实就是使用synchronized关键字。所以说volatile关键字本身并不处理数据的原子性,而是强制对数据的读写及时影响到主内存中。

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

除了在i++操作时使用synchronized关键字实现同步外,还可以使用AtomicInteger原子类进行实现。

原子操作是不可分割的整体,没有其他线程能够中断或检查正在原子操作中的变量。它可以在没有锁的情况下做到线程安全。

示例代码:

public class MyThread extends Thread {
    private static AtomicInteger count = new AtomicInteger(0);
    @Override
    public void run() {
        addCount();
    }

    private static void addCount() {
        for (int i = 0;i<100;i++){
            System.out.println(count.incrementAndGet());
        }
    }

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

打印结果:

....
9996
9997
9998
9999
10000

成功达到累加的效果。

3.5 原子类也不安全

原子类在具有有逻辑性的情况下输出结果也具有随机性。

示例代码:

public class MyThread extends Thread {
    private static AtomicInteger count = new AtomicInteger(0);

    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        this.addCount();
    }

    private void addCount() {
        System.out.println(Thread.currentThread().getName()+"加100之后:"+count.addAndGet(100));
        count.addAndGet(1);
    }

    public static void main(String[] args) throws InterruptedException {
        MyThread[] myThreads = new MyThread[10];
        for (int i = 0; i < 10; i++) {
            myThreads[i] = new MyThread("Thread-"+i);
        }
        for (int i = 0; i < 10; i++) {
            myThreads[i].start();
        }
        Thread.sleep(2000);
        System.out.println(MyThread.count);
    }
}

打印结果:

Thread-0加100之后:100
Thread-2加100之后:201
Thread-1加100之后:302
Thread-5加100之后:602
Thread-4加100之后:502
Thread-3加100之后:402
Thread-6加100之后:706
Thread-7加100之后:807
Thread-9加100之后:908
Thread-8加100之后:1009
1010

可以看到,结果值正确但是打印顺序出错了,出现这样的原因是因为AtomicInteger的addAndGet()方法是原子的,但方法与方法之间的调用却不是原子的。也就是方法addCount的调用不是原子的。解决这样的问题必须要用同步。

3.6 synchronized代码块有volatile同步的功能

关键字synchronized可以使多个线程访问同一个资源具有同步性,而且它还具有将线程工作内存中的私有变量与公共内存中的变量同步的功能。

我们把前面讲到的异步死循环代码改造一下:

public class PrintString implements Runnable{
    private boolean isRunnning = true;

    @Override
    public void run() {
        String lock = new String();
        System.out.println("Thread begin: "+Thread.currentThread().getName());
        while (isRunnning == true){
            synchronized (lock){
                //加与不加的效果就是是否死循环
            }
        }
        System.out.println("Thread end: "+Thread.currentThread().getName());
    }

    public boolean isRunnning() {
        return isRunnning;
    }

    public void setRunnning(boolean runnning) {
        isRunnning = runnning;
    }

    public static void main(String[] args) throws InterruptedException {
        PrintString printString = new PrintString();
        Thread thread = new Thread(printString,"Thread-A");
        thread.start();
        Thread.sleep(1000);
        printString.setRunnning(false);
        System.out.println("我要停止它!" + Thread.currentThread().getName());
    }

}

打印结果:

Thread begin: Thread-A
我要停止它!main
Thread end: Thread-A

关键字synchronized可以保证在同一时刻,只有一个线程可以执行某一个方法或某一个代码块。它包含两个特征:互斥相和可见性。同步synchronized不仅可以解决一个线程看到对象处于不一致的状态,还可以保证进入同步方法或者同步代码块的每个线程,都看到由同一个锁保护之前所有的修改效果。

学习多线程并发。要着重“外修互斥,内修可见”,这是掌握多线程、学习多线程并发的重要技术点。

文章来源:微信公众号 薛勤的博客

上一篇:FusionCharts 2D柱状图和折线图的组合图调试错误


下一篇:uva10635 Prince and Princess(dp)