天天看點

關于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會會使線程進入阻塞。