关于JAVA中volatile使用的一些总结

简介: 太敏感的人会体谅到他人的痛苦,自然就无法轻易做到坦率。所谓的坦率,其实就是暴力。-----太宰治《候鸟》

写在前面


我的需求

  • CSDN看到一个小伙伴问了这样JAVA并发的问题,然后我做了解答,主要使用了volatile
(1)某电影放映厅一共有10排,每排10个座位,座位号为“排号+列号”,如第8排,座位号是8A-8J;
(2)此放映厅某一场次现有100张票要卖出,观众可以通过四个渠道购票:电影院、时光网、美团和支付宝;
(3)各个售票点的效率不同,每卖出一张票,各个售票点所需要的时间分别为:电影院3秒,时光网5秒,美团2秒,支付宝6秒;

现在这4个售票点同时售票,根据以上信息,用多线程模拟这4个售票点的售票情况。要求打印出每个售票点所卖出电影票的座位号,座位号随机确定。

我需要解决的问题:

  • 答完之后他反馈有问题,我测了几次,发现确实有问题。会有打印重票的时候,对于volatile的理解有些问题

我是这样做的:

  • 微信群里问了大佬。使用了原子类(atomic)解决这个问题。
  • 这里对volatile总结一下,当然没有涉及啥底层的东西,很浅。

太敏感的人会体谅到他人的痛苦,自然就无法轻易做到坦率。所谓的坦率,其实就是暴力。-----太宰治《候鸟》


我们先来看看他这到题,座位号随机确定我们直接用数子自增模拟,没有实现,想实现的话,可以把所有的号码随机初始化一个Set,然后每次pull一个出来。

下面是我最开始的解决方案,使用volatile来处理线程安全问题,认为我每次都可以拿到最新的,即可以满足线程安全。但是打印出来的数据有重复的,忽略了volatile修饰变量不满足原子性的问题,而 index++本身也不是原子操作,所以会有重票的问题


import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;


/**
 * @Classname Ticket
 * @Description TODO
 * @Date 2021/12/8 19:00
 * @Created LiRuilong
 */
public class Ticket implements Runnable {
    //最多受理100笔业务
    private static final int MAX = 100;
    // 开始业务
    //static AtomicInteger index = new AtomicInteger(0);
    private static   volatile int index = 0;
    // 电影院3秒,时光网5秒,美团2秒,支付宝6秒;
    private static volatile Map<String, ArrayList> map = new HashMap() {{
        put("电影院", new ArrayList<>());
        put("时光网", new ArrayList<>());
        put("美团", new ArrayList<>());
        put("支付宝", new ArrayList<>());
    }};

    @Override
    public void run() {
        while (index < MAX) {
            try {
                String currentThreadName = Thread.currentThread().getName();
                switch (currentThreadName) {
                    case "电影院": {
                        map.get("电影院").add(++index);
                        TimeUnit.MILLISECONDS.sleep(3);
                    }
                    break;
                    case "时光网": {
                        map.get("时光网").add(++index);
                        TimeUnit.MILLISECONDS.sleep(5);
                    }
                    break;
                    case "美团": {
                        map.get("美团").add(++index);
                        TimeUnit.MILLISECONDS.sleep(2);
                    }
                    break;
                    case "支付宝": {
                        map.get("支付宝").add(++index);
                        TimeUnit.MILLISECONDS.sleep(6);
                    }
                    break;
                    default:
                        break;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {

        final Ticket task = new Ticket();
        //电影院3秒,时光网5秒,美团2秒,支付宝6秒;
        new Thread(task, "电影院").start();
        new Thread(task, "时光网").start();
        new Thread(task, "美团").start();
        new Thread(task, "支付宝").start();

        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                map.forEach((o1, o2) -> {
                    System.out.println(o1 + " | 总票数:"+o2.size()+o2.stream().reduce(":", (a, b) -> a + " " + b));
                });
            }
        });
    }
}

==========================
支付宝 | 总票数:18: 2 8 13 18 24 30 36 41 47 53 58 64 69 77 82 87 94 100
电影院 | 总票数:32: 3 4 7 10 12 16 19 23 25 27 32 35 37 40 43 46 49 52 56 58 62 65 68 73 76 80 83 86 89 92 95 98
时光网 | 总票数:20: 1 5 10 14 20 24 28 34 38 44 48 54 59 64 71 75 81 85 91 97
美团 | 总票数:43: 2 4 6 9 11 12 15 17 21 22 24 26 29 31 33 36 37 39 42 45 47 50 51 55 57 60 61 63 66 67 70 72 74 78 79 82 84 86 88 90 93 96 99

后来通过使用原子类,使用了AtomicInteger来满足index++的原子性,票数可以正常的打印出来

package com.ztesoft.pwd.bo;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @Classname Ticket
 * @Description TODO
 * @Date 2021/12/8 19:00
 * @Created LiRuilong
 */
public class Ticket implements Runnable {
    //最多受理100笔业务
    private static final int MAX = 100;
    // 开始业务
    static AtomicInteger index = new AtomicInteger(0);
    //private static   volatile int index = 0;
    // 电影院3秒,时光网5秒,美团2秒,支付宝6秒;
    private static volatile Map<String, ArrayList> map = new HashMap() {{
        put("电影院", new ArrayList<>());
        put("时光网", new ArrayList<>());
        put("美团", new ArrayList<>());
        put("支付宝", new ArrayList<>());
    }};

    @Override
    public void run() {
        while (index.get() < MAX) {
            try {
                String currentThreadName = Thread.currentThread().getName();
                switch (currentThreadName) {
                    case "电影院": {
                        map.get("电影院").add(index.addAndGet(1));
                        TimeUnit.MILLISECONDS.sleep(3);
                    }
                    break;
                    case "时光网": {
                        map.get("时光网").add(index.addAndGet(1));
                        TimeUnit.MILLISECONDS.sleep(5);
                    }
                    break;
                    case "美团": {
                        map.get("美团").add(index.addAndGet(1));
                        TimeUnit.MILLISECONDS.sleep(2);
                    }
                    break;
                    case "支付宝": {
                        map.get("支付宝").add(index.addAndGet(1));
                        TimeUnit.MILLISECONDS.sleep(6);
                    }
                    break;
                    default:
                        break;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {

        final Ticket task = new Ticket();
        //电影院3秒,时光网5秒,美团2秒,支付宝6秒;
        new Thread(task, "电影院").start();
        new Thread(task, "时光网").start();
        new Thread(task, "美团").start();
        new Thread(task, "支付宝").start();

        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                map.forEach((o1, o2) -> {
                    System.out.println(o1 + " | 总票数:"+o2.size()+o2.stream().reduce(":", (a, b) -> a + " " + b));
                });
            }
        });
    }
}

=================================
支付宝 | 总票数:16: 4 10 16 22 29 36 41 47 55 61 68 73 80 87 93 99
电影院 | 总票数:27: 2 6 9 13 17 21 24 27 32 34 39 42 46 49 52 57 59 64 67 71 75 77 82 86 90 92 97
时光网 | 总票数:17: 3 8 14 19 26 30 37 44 48 54 60 65 72 78 84 89 96
美团 | 总票数:40: 1 5 7 11 12 15 18 20 23 25 28 31 33 35 38 40 43 45 50 51 53 56 58 62 63 66 69 70 74 76 79 81 83 85 88 91 94 95 98 100

关于volatile的使用

被volatile关键字修饰的实例变量或者类变量具备两层语义:

  • 保证了不同线程之间对共享变量的可见性,
  • 禁止对volatile变量进行重排序。

每个线程都运行在栈内存中,每个线程都有自己的工作内存(Working Memory),比如寄存器Register,高速缓存存储器Cache等,线程的计算一般是通过工作内存进行交互的,线程在初始化时从主内存中加载所需要的变量值到工作内存中,然后在线程运行时,如果读取内存,则直接从工作内存中读取,若是写入则先写入到工作内存中,之后在刷新到主内存中。

在多线程情况下,可能读到的不是最新的值,可以使用synchronized同步代码块,或使用Lock锁来解决该问题。JAVA可以使用volatile解决,在变量前加volatile关键字,可以保证每个线程对本地变量的访问和修改都是直接与主内存交互的,而不是与本线程的工作内存交互

但是Volatile关键字并不能保证线程安全,换句话讲它只能保证当前线程需要该变量的值能够获得最新的值,而不能保证多个线程修改的安全性。

使用 volatile,需要保证:

  • 对变量的写操作不依赖于当前值;
  • 该变量没有包含在具有其他变量的不变式中

关于volatile的一些基本概念

volatile关键字只能修饰类变量和实例变量,对于方法参数,局部变量已及实例常量,类常量都不能进行修饰。

原子性,有序性和可见性

并发编程的三个重要的特性

可见性 有序性 原子性
当一个线程对共享变量进行了修改,那么另一个变量可以立即看到。volatile具有保证可见性的语义 Java在运行期会对代码进行优化,执行顺序未必就是编译顺序, volatile具有保证有序性的语义。 多个原子性的操作在一起就不再是原子性操作了。
Java提供了以下三种方式来保证可见性 Java提供了三种保证有序性的方式,具体如下 简单的读取与赋值操作是原子性的,将一个变量赋给另外一个变量的操作不是原子性的。
使用关键字volatile,当一个变量被volatile关键字修饰时,对于共享资源的读操作会直接在主内存中进行(当然也会缓存到工作内存中,当其他线程对该共享资源进行了修改,则会导致当前线程在工作内存中的共享资源失效,所以必须从主内存中再次获取),对于共享资源的写操作当然是先要修改工作内存,但是修改结束后会立刻将其刷新到主内存中。
通synchronized 关键字实现,同步块保证任何时候只有一个线程获得锁,然后执行同步方法,并且还会确保在锁释放之前,会将对变量的修改刷新到主内存当中
通过JUC提供的显式锁Lock也能够保证可见性, Lock的lock方法能够保证在同一时刻只有一个线程获得锁然后执行同步方法,并且会确保在锁释放(Lock的unlock方法)之前会将对变量的修改刷新到主内存当中。
使用volatile关键字来保证有序性。使用synchronized关键字来保证有序性。使用显式锁Lock来保证有序性。 Java内存模型(JMM)只保证了基本读取和赋值的原子性操作,其他的均不保证,如果想要使得某些代码片段具备原子性,需要使用关键字synchronized,或者JUC中的lock。如果想要使得int等类型自增操作具备原子性,可以使用JUC包下的原子封装类型java.util.concurrent.atomic.*

volatile和synchronized区别

区别 描述
使用上区别 volatile关键字只能用来修饰实例变量或者类变量,不能修饰方法已及方法参数和局部变量和常量。
synchronized关键字不能用来修饰变量,只能用于修饰方法和语句块。
volatile修饰的变量可以为空,同步块的monitor不能为空。
对原子性的保证 volatile无法保证原子性
synchronizde能够保证。因为无法被中途打断。
对可见性的保证 都可以实现共享资源的可见性,但是实现的机制不同
synchronized借助于JVM指令monitor enter 和monitor exit ,通过排他的机制使线程串行通过同步块,在monitor退出后所共享的内存会被刷新到主内存中。
volatile使用机器指令(硬编码)的方式,lock迫使其他线程工作内存中的数据失效,不得不主内存继续加载。
对有序性的保证 volatile关键字禁止JVM编译器已及处理器对其进行重排序,能够保证有序性。
synchronized保证顺序性是串行化的结果,但同步块里的语句是会发生指令从排。
其他 volatile不会使线程陷入阻塞
synchronized会会使线程进入阻塞。
相关文章
|
4月前
|
存储 安全 Java
Java面试题:深入探索Java内存模型,Java内存模型中的主内存与工作内存的概念,Java内存模型中的happens-before关系,volatile关键字在Java内存模型中的作用
Java面试题:深入探索Java内存模型,Java内存模型中的主内存与工作内存的概念,Java内存模型中的happens-before关系,volatile关键字在Java内存模型中的作用
38 1
|
3月前
|
存储 SQL 缓存
揭秘Java并发核心:深度剖析Java内存模型(JMM)与Volatile关键字的魔法底层,让你的多线程应用无懈可击
【8月更文挑战第4天】Java内存模型(JMM)是Java并发的核心,定义了多线程环境中变量的访问规则,确保原子性、可见性和有序性。JMM区分了主内存与工作内存,以提高性能但可能引入可见性问题。Volatile关键字确保变量的可见性和有序性,其作用于读写操作中插入内存屏障,避免缓存一致性问题。例如,在DCL单例模式中使用Volatile确保实例化过程的可见性。Volatile依赖内存屏障和缓存一致性协议,但不保证原子性,需与其他同步机制配合使用以构建安全的并发程序。
71 0
|
4月前
|
缓存 安全 算法
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
47 0
|
5月前
|
缓存 安全 Java
《volatile使用与学习总结:》多层面分析学习java关键字--volatile
《volatile使用与学习总结:》多层面分析学习java关键字--volatile
32 0
|
27天前
|
SQL 缓存 安全
[Java]volatile关键字
本文介绍了Java中volatile关键字的原理与应用,涵盖JMM规范、并发编程的三大特性(可见性、原子性、有序性),并通过示例详细解析了volatile如何实现可见性和有序性,以及如何结合synchronized、Lock和AtomicInteger确保原子性,最后讨论了volatile在单例模式中的经典应用。
34 0
|
2月前
|
缓存 Java 编译器
JAVA并发编程volatile核心原理
volatile是轻量级的并发解决方案,volatile修饰的变量,在多线程并发读写场景下,可以保证变量的可见性和有序性,具体是如何实现可见性和有序性。以及volatile缺点是什么?
|
3月前
|
安全 Java 编译器
Java 中的 volatile 变量
【8月更文挑战第22天】
27 4
|
3月前
|
缓存 安全 Java
Java里为什么单利一定要加volatile呢?
【8月更文挑战第11天】Java里为什么单利一定要加volatile呢?
30 3
|
3月前
|
缓存 安全 Java
Java里volatile底层是如何实现的?
【8月更文挑战第11天】Java里的volatile底层是如何实现的?
28 2
|
3月前
|
安全 Java