Java——多线程高并发系列之volatile关键字

简介: Java——多线程高并发系列之volatile关键字

文章目录:


写在前面:synchronizedvolatile关键字的作用、区别?

Demo1(不使用volatile,不保证可见性)

Demo2(使用volatile,保证可见性)

Demo3volatile不保证原子性)

写在前面:synchronized和volatile关键字的作用、区别?


一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

保证了不同线程对这个变量进行操作时的可见性,不保证原子性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

禁止进行指令重排序。

·       volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。

·       volatile仅能使用在变量级别;synchronized则可以使用在变量、方法级别的。

·       volatile仅能实现变量的修改可见性,并不能保证原子性;synchronized则可以保证变量的修改可见性和原子性。

·       volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。

·       volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。


Demo1(不使用volatile,不保证可见性)


我们说volatile只能用来修饰变量,那么这个例子中,没有使用volatile修饰静态内部类中的continuePrint变量,它的初始值为true,在main线程中,开启子线程之后,它会去执行printStringMethod方法,然后打印打印一句话,之后是一个循环whilecontinuePrint {} ,对于这个子线程来说,它的这个while循环是一直成立的,所以它会一直在这执行这个空内容的while循环。而main睡眠1秒之后,它将continuePrint的值修改为了false,那么这个时候子线程能否知道、并且终止while循环呢?答案是不能,因为continuePrint这个局部变量是子线程独占的,我们都知道局部变量存储在栈空间中,而每个线程的栈空间都是独立不共享的,它们共享的仅仅是堆区和方法区。所以即使你的main主线程修改了continuePrint变量为false,但是在子线程的眼中,continuePrint变量仍然为true。所以执行结果中就会一直卡在这里了。

package com.szh.volatiletest;
/**
 * volatile保证可见性
 */
public class Test01 {
    public static void main(String[] args) {
        PrintString ps=new PrintString();
        //开启子线程,让子线程执行 ps 对象中的 printStringMethod() 方法
        new Thread(new Runnable() {
            @Override
            public void run() {
                ps.printStringMethod();
            }
        }).start();
        //main线程睡眠1000ms
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("在main线程中修改打印标志");
        /*
            main线程在这里对 continuePrint 做了修改,但是子线程中是读取不到的
            解决办法:使用 volatile 关键字修饰 continuePrint
                    volatile 关键字的作用可以强制线程从公共内存中读取数据,而不是从工作内存中读取
         */
        ps.setContinuePrint(false);
    }
    static class PrintString {
        private boolean continuePrint=true;
        public PrintString setContinuePrint(boolean continuePrint) {
            this.continuePrint=continuePrint;
            return this;
        }
        public void printStringMethod() {
            System.out.println(Thread.currentThread().getName() + "开始......");
            while (continuePrint) {
            }
            System.out.println(Thread.currentThread().getName() + "结束......");
        }
    }
}


那么,如何才能使main线程修改完continuePrint变量的值之后,在子线程中也可以读取到呢?答案就是使用 volatile 关键字。

Demo2(使用volatile,保证可见性)


首先说一下,volatile 关键字的作用就是使变量在多个线程之间是可见的!!!

在上个例子的基础上,我们将continuePrint变量的修饰符中添加上 volatile,那么这个变量对于main主线程、Thread-0子线程都是可见的了。也就是说,当Thread-0子线程执行到whilecontinuePrint{} 时,先执行一会这个空的循环体,然后main主线程将continuePrint变量修改为了false,这个时候,Thread-0子线程就可以读取到continuePrint变量被修改为了false,那么while循环就不成立了,自然而然的就结束了。

package com.szh.volatiletest;
/**
 * volatile保证可见性
 */
public class Test01 {
    public static void main(String[] args) {
        PrintString ps=new PrintString();
        //开启子线程,让子线程执行 ps 对象中的 printStringMethod() 方法
        new Thread(new Runnable() {
            @Override
            public void run() {
                ps.printStringMethod();
            }
        }).start();
        //main线程睡眠1000ms
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("在main线程中修改打印标志");
        /*
            main线程在这里对 continuePrint 做了修改,但是子线程中是读取不到的
            解决办法:使用 volatile 关键字修饰 continuePrint
                    volatile 关键字的作用可以强制线程从公共内存中读取数据,而不是从工作内存中读取
         */
        ps.setContinuePrint(false);
    }
    static class PrintString {
        private volatile boolean continuePrint=true;
        public PrintString setContinuePrint(boolean continuePrint) {
            this.continuePrint=continuePrint;
            return this;
        }
        public void printStringMethod() {
            System.out.println(Thread.currentThread().getName() + "开始......");
            while (continuePrint) {
            }
            System.out.println(Thread.currentThread().getName() + "结束......");
        }
    }
}

Demo3(volatile不保证原子性)


这个例子演示volatile不保证原子性。

main主线程中创建10个子线程,这10个子线程分别去执行1000count++操作,那么根据简单的逻辑推理,这10个子线程的执行结果应该是类似于:1000400080002000......这样的,都是10的整数倍,但是从输出结果中看到,并非如此。

这里的原因可能有:其中一个子线程的for循环还未执行完,另外的子线程就抢走了你的CPU执行权,另外的子线程开始执行它的for循环了,而此时的count变量因为有volatile修饰,所以count对其他子线程是可见的。

                              其中一个子线程的count++操作执行到一半时,被另外的子线程抢走了CPU的执行权。(所以说这里的count++并不是原子性操作)

那么,Java中有两种方式实现原子性: 一种是使用锁;  另一种利用处理器的 CAS(Compare and Swap)指令。

锁具有排它性,保证共享变量在某一时刻只能被一个线程访问。CAS 指令直接在硬件(处理器和内存)层次上实现,看作是硬件锁。

package com.szh.volatiletest;
/**
 * volatile不保证原子性
 */
public class Test02 {
    public static void main(String[] args) {
        //在main线程中创建10个子线程
        for (int i = 0; i < 10; i++) {
            new MyThread().start();
        }
    }
    static class MyThread extends Thread {
        public volatile static int count;
        public static void addCount() {
            for (int i = 0; i < 1000; i++) {
                count++;
            }
            System.out.println(Thread.currentThread().getName() + " count = " + count);
        }
        @Override
        public void run() {
            addCount();
        }
    }
}

目录
打赏
0
0
0
0
85
分享
相关文章
Java网络编程,多线程,IO流综合小项目一一ChatBoxes
**项目介绍**:本项目实现了一个基于TCP协议的C/S架构控制台聊天室,支持局域网内多客户端同时聊天。用户需注册并登录,用户名唯一,密码格式为字母开头加纯数字。登录后可实时聊天,服务端负责验证用户信息并转发消息。 **项目亮点**: - **C/S架构**:客户端与服务端通过TCP连接通信。 - **多线程**:采用多线程处理多个客户端的并发请求,确保实时交互。 - **IO流**:使用BufferedReader和BufferedWriter进行数据传输,确保高效稳定的通信。 - **线程安全**:通过同步代码块和锁机制保证共享数据的安全性。
73 23
|
1月前
|
Volatile关键字与Java原子性的迷宫之旅
通过合理使用 `volatile`和原子操作,可以在提升程序性能的同时,确保程序的正确性和线程安全性。希望本文能帮助您更好地理解和应用这些并发编程中的关键概念。
44 21
课时8:Java程序基本概念(标识符与关键字)
课时8介绍Java程序中的标识符与关键字。标识符由字母、数字、下划线和美元符号组成,不能以数字开头且不能使用Java保留字。建议使用有意义的命名,如student_name、age。关键字是特殊标记,如蓝色字体所示。未使用的关键字有goto、const;特殊单词null、true、false不算关键字。JDK1.4后新增assert,JDK1.5后新增enum。
【JAVA】封装多线程原理
Java 中的多线程封装旨在简化使用、提高安全性和增强可维护性。通过抽象和隐藏底层细节,提供简洁接口。常见封装方式包括基于 Runnable 和 Callable 接口的任务封装,以及线程池的封装。Runnable 适用于无返回值任务,Callable 支持有返回值任务。线程池(如 ExecutorService)则用于管理和复用线程,减少性能开销。示例代码展示了如何实现这些封装,使多线程编程更加高效和安全。
Java中的this关键字详解:深入理解与应用
本文深入解析了Java中`this`关键字的多种用法
261 9
利用 Java 代码获取淘宝关键字 API 接口
在数字化商业时代,精准把握市场动态与消费者需求是企业成功的关键。淘宝作为中国最大的电商平台之一,其海量数据中蕴含丰富的商业洞察。本文介绍如何通过Java代码高效、合规地获取淘宝关键字API接口数据,帮助商家优化产品布局、制定营销策略。主要内容包括: 1. **淘宝关键字API的价值**:洞察用户需求、优化产品标题与详情、制定营销策略。 2. **获取API接口的步骤**:注册账号、申请权限、搭建Java开发环境、编写调用代码、解析响应数据。 3. **注意事项**:遵守法律法规与平台规则,处理API调用限制。 通过这些步骤,商家可以在激烈的市场竞争中脱颖而出。
Java 多线程 面试题
Java 多线程 相关基础面试题
|
1月前
|
Linux编程: 在业务线程中注册和处理Linux信号
本文详细介绍了如何在Linux中通过在业务线程中注册和处理信号。我们讨论了信号的基本概念,并通过完整的代码示例展示了在业务线程中注册和处理信号的方法。通过正确地使用信号处理机制,可以提高程序的健壮性和响应能力。希望本文能帮助您更好地理解和应用Linux信号处理,提高开发效率和代码质量。
57 17
|
1月前
|
Linux编程: 在业务线程中注册和处理Linux信号
通过本文,您可以了解如何在业务线程中注册和处理Linux信号。正确处理信号可以提高程序的健壮性和稳定性。希望这些内容能帮助您更好地理解和应用Linux信号处理机制。
63 26
|
3月前
|
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
323 2

热门文章

最新文章