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

简介: 关键字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,所以一直就是死循环的状态。内存结构图:

image.png

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

将代码更改如下:

volatile private boolean isRunnning = true;

再次运行:

Thread begin: Thread-A

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

通过使用volatile关键字,强制的从公共内存中读取变量的值,内存结构如图所示:
image.png

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

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

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

目录
相关文章
|
1天前
|
安全 Java 开发者
Java并发编程之深入理解synchronized关键字
【6月更文挑战第26天】在Java并发编程的世界中,synchronized关键字扮演着守护者的角色,它保护着共享资源的完整性。本文将深入探讨synchronized关键字的内部机制、使用方法及其对性能的影响,帮助开发者更好地利用这一工具来构建线程安全的应用。
|
1天前
|
数据采集 Java Unix
10-多线程、多进程和线程池编程(2)
10-多线程、多进程和线程池编程
|
1天前
|
安全 Java 调度
10-多线程、多进程和线程池编程(1)
10-多线程、多进程和线程池编程
|
21小时前
|
缓存 Java 编译器
必知的技术知识:Java并发编程:volatile关键字解析
必知的技术知识:Java并发编程:volatile关键字解析
|
21小时前
|
缓存 安全 Java
《volatile使用与学习总结:》多层面分析学习java关键字--volatile
《volatile使用与学习总结:》多层面分析学习java关键字--volatile
6 0
|
1天前
|
Java 数据库连接 调度
Java多线程,对锁机制的进一步分析
Java多线程,对锁机制的进一步分析
|
1天前
|
Java
Java多线程notifyAll()方法
Java多线程notifyAll()方法
|
1天前
|
Java 测试技术 开发者
Java并发编程:深入理解线程池
本文将带领读者深入了解Java中的线程池,探索其内部机制、使用场景以及如何有效地利用线程池来提高程序的性能和可维护性。我们将通过实例演示如何创建和配置线程池,并讨论常见的并发模式和最佳实践。文章旨在为开发者提供实用的线程池应用知识,帮助他们在面对多线程编程挑战时,能够设计出更加高效和稳定的系统。
|
2天前
|
缓存 安全 Java
如何使用Java实现高效的多线程编程
如何使用Java实现高效的多线程编程
|
1月前
|
安全 Java
深入理解Java并发编程:线程安全与性能优化
【2月更文挑战第22天】在Java并发编程中,线程安全和性能优化是两个重要的主题。本文将深入探讨这两个主题,包括线程安全的基本概念,如何实现线程安全,以及如何在保证线程安全的同时进行性能优化。
26 0