寫在前面
我的需求:
-
看到一個小夥伴問了這樣JAVA并發的問題,然後我做了解答,主要使用了CSDN
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關鍵字修飾的執行個體變量或者類變量具備兩層語義:
- 保證了不同線程之間對共享變量的可見性,
- 禁止對volatile變量進行重排序。
每個線程都運作在棧記憶體中,每個線程都有自己的工作記憶體(Working Memory),比如寄存器Register,高速緩存存儲器Cache等,線程的計算一般是通過工作記憶體進行互動的,線程在初始化時從主記憶體中加載所需要的變量值到工作記憶體中,然後線上程運作時,如果讀取記憶體,則直接從工作記憶體中讀取,若是寫入則先寫入到工作記憶體中,之後在重新整理到主記憶體中。
在多線程情況下,可能讀到的不是最新的值,可以使用
synchronized
同步代碼塊,或使用
Lock
鎖來解決該問題。
JAVA
可以使用
volatile
解決,在變量前加
volatile
關鍵字,可以保證
每個線程對本地變量的通路和修改都是直接與主記憶體互動的,而不是與本線程的工作記憶體互動
。
但是Volatile關鍵字并不能保證線程安全,換句話講它隻能保證目前線程需要該變量的值能夠獲得最新的值,而不能保證多個線程修改的安全性。
使用
volatile
,需要保證:
- 對變量的寫操作不依賴于目前值;
- 該變量沒有包含在具有其他變量的不變式中
volatile
的一些基本概念
volatile
volatile關鍵字隻能修飾類變量和執行個體變量,對于方法參數,局部變量已及執行個體常量,類常量都不能進行修飾。
原子性,有序性和可見性
并發程式設計的三個重要的特性
可見性 | 有序性 | 原子性 |
---|---|---|
當一個線程對共享變量進行了修改,那麼另一個變量可以立即看到。 具有保證可見性的語義 | Java在運作期會對代碼進行優化,執行順序未必就是編譯順序, 具有保證有序性的語義。 | 多個原子性的操作在一起就不再是原子性操作了。 |
Java提供了以下三種方式來保證可見性 | Java提供了三種保證有序性的方式,具體如下 | 簡單的讀取與指派操作是原子性的,将一個變量賦給另外一個變量的操作不是原子性的。 |
使用關鍵字volatile,當一個變量被volatile關鍵字修飾時,對于共享資源的讀操作會直接在主記憶體中進行(當然也會緩存到工作記憶體中,當其他線程對該共享資源進行了修改,則會導緻目前線程在工作記憶體中的共享資源失效,是以必須從主記憶體中再次擷取),對于共享資源的寫操作當然是先要修改工作記憶體,但是修改結束後會立刻将其重新整理到主記憶體中。 通synchronized 關鍵字實作,同步塊保證任何時候隻有一個線程獲得鎖,然後執行同步方法,并且還會確定在鎖釋放之前,會将對變量的修改重新整理到主記憶體當中 通過JUC提供的顯式鎖Lock也能夠保證可見性, Lock的lock方法能夠保證在同一時刻隻有一個線程獲得鎖然後執行同步方法,并且會確定在鎖釋放(Lock的unlock方法)之前會将對變量的修改重新整理到主記憶體當中。 | 關鍵字來保證有序性。使用 來保證有序性。 | Java記憶體模型(JMM)隻保證了基本讀取和指派的原子性操作,其他的均不保證,如果想要使得某些代碼片段具備原子性,需要使用關鍵字 ,或者 中的 。如果想要使得int等類型自增操作具備原子性,可以使用 下的原子封裝類型 |
volatile和synchronized差別
差別 | 描述 |
---|---|
使用上差別 | volatile關鍵字隻能用來修飾執行個體變量或者類變量,不能修飾方法已及方法參數和局部變量和常量。 synchronized關鍵字不能用來修飾變量,隻能用于修飾方法和語句塊。 volatile修飾的變量可以為空,同步塊的monitor不能為空。 |
對原子性的保證 | volatile無法保證原子性 synchronizde能夠保證。因為無法被中途打斷。 |
對可見性的保證 | 都可以實作共享資源的可見性,但是實作的機制不同 synchronized借助于JVM指令monitor enter 和monitor exit ,通過排他的機制使線程串行通過同步塊,在monitor退出後所共享的記憶體會被重新整理到主記憶體中。 volatile使用機器指令(寫死)的方式, 迫使其他線程工作記憶體中的資料失效,不得不主記憶體繼續加載。 |
對有序性的保證 | volatile關鍵字禁止JVM編譯器已及處理器對其進行重排序,能夠保證有序性。 synchronized保證順序性是串行化的結果,但同步塊裡的語句是會發生指令從排。 |
其他 | volatile不會使線程陷入阻塞 synchronized會會使線程進入阻塞。 |