JUC并發編程(十)--Volatile、原子性以及單例模式的應用
- 一、JMM
- 1、什么是JMM
- 2、JMM的約定
- 3、八種操作
- 二、Volatile
- 1、可見性
- 2、不保證原子性
- 原子類
- 3、禁止指令重排
- 三、單例模式
- 1、常見的懶漢模式
- 2、破解一般的懶漢模式
- 3、使用列舉實作單例模式
一、JMM
1、什么是JMM
JMM是一種Java記憶體模型,是一種概念性的約定,而不是實際存在的東西,
2、JMM的約定
-
執行緒解鎖前,必須把變數立即刷回主存;
我們知道,一個執行緒作業時,會將主存中的變數復制一份給執行緒自己的記憶體,作為一個副本,執行緒對此變數的操作,都是在副本上的操作,所以當執行緒運行完畢,解鎖的時候,必須將副本的值同步回主存, -
執行緒加鎖前,必須讀取主存中最新的變數值,然后復制到自己的作業記憶體中;
-
加鎖和解鎖是同一把鎖,
3、八種操作

- 從主存中read操作和load到執行緒作業記憶體中,是一組操作;
- 執行緒的執行引擎使用此變數,并在使用之后回傳此變數,也是一組操作;
- 從作業記憶體中保存此變數(store操作),并寫回主存,也是一組操作;
- 加鎖(lock)和解鎖(unlock),也是一組操作,

這里有個問題,現在執行緒A和執行緒B都從主存中讀取了Flag,然后執行緒B先修改了值,并寫回了記憶體,這時執行緒A不能及時得到此訊息,即不能及時可見此變數,所以這時我們就需要Volatile了,
二、Volatile
1、可見性
先上代碼:
package com.zhan.juc.volatiletest;
import java.util.concurrent.TimeUnit;
/**
* @Author Zhanzhan
* @Date 2021/1/28 21:07
*/
public class JMMDemo {
private static int num = 0;
public static void main(String[] args){
new Thread(() -> {
while (num == 0){
}
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
num = 1;
System.out.println(num);
}
}
我們看上面的代碼,可以看到,有一個執行緒,里面while回圈,不停的判斷num的值,然后在執行緒的下面將num值賦為1,那么這時我們開出來的這個執行緒會停掉嗎?
看結果:

我們發現,并沒有停,這是為什么呢?就像上面說的那樣,執行緒將num的值從主存中復制了一份,然后主存中num值的變動,執行緒并沒有看到,
那么要如何解決?接下來看:
package com.zhan.juc.volatiletest;
import java.util.concurrent.TimeUnit;
/**
* @Author Zhanzhan
* @Date 2021/1/28 21:07
*/
public class JMMDemo {
private volatile static int num = 0;
public static void main(String[] args){
new Thread(() -> {
while (num == 0){
}
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
num = 1;
System.out.println(num);
}
}
我們看到,上面的代碼中num用了volatile來修飾,然后看結果:

我們發現,結果符合我們的預期,執行緒看到了主存中num的值的變動,
2、不保證原子性
什么是原子性:不可分割,執行緒A在執行任務的時候,不能被打擾,也不能被分割,要么同時成功,要么同時失敗,
廢話不多說,上代碼:
package com.zhan.juc.volatiletest;
/**
* 測驗volatile不保證原子性
*
* @Author Zhanzhan
* @Date 2021/1/28 21:19
*/
public class Demo {
private volatile static int num = 0;
public static void add() {
num++;
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
// 判斷當前的執行緒是否大于兩條
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + " num=" + num);
}
}
如果執行緒能保證原子性,那么輸出的結果應該是num=20000,現在我們看結果:

發現,volatile不能保證原子性,
那這時我們就有一個問題,如果不使用lock和synchronized,怎么保證原子性?
原子類
上代碼:
package com.zhan.juc.volatiletest;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 測驗volatile不保證原子性
*
* @Author Zhanzhan
* @Date 2021/1/28 21:19
*/
public class Demo {
// private volatile static int num = 0;
private volatile static AtomicInteger num = new AtomicInteger();
public static void add() {
// num++; // 不是一個原子性操作
num.getAndIncrement();
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
// 判斷當前的執行緒是否大于兩條
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + " num=" + num);
}
}
看結果:

3、禁止指令重排
什么是指令重排:計算機并不是按照我們寫的程式的順序那樣去執行的,會進行指令優化,
源代碼 =》編譯器優化的重排=》指令并行也可能的重排=》記憶體系統的重排=》執行
處理器在進行指令重排的時候,會考慮資料之間的依賴性,
int x = 1; // 1
int y = 2; // 2
x = x + 5; // 3
y = x * x; // 4
我們期望的順序:1234
但可能編程的執行順序:2134 1324
舉例說明,指令重排可能引發的問題:
假設有兩個執行緒A和B,然后有四個變數:a b x y
a b x y四個值默認為0
| 執行緒A | 執行緒B |
|---|---|
| x = a | y = b |
| b = 1 | a = 2 |
執行緒A執行了兩個操作:x = a 和 b = 1;
執行緒B執行了兩個操作:y = b 和 a = 2;
在執行緒A執行 x = a后,執行緒B執行了 y = b,
這時我們預期的結果是 x = 0 和 y = 0;
但是現在執行緒A中的變數沒有依賴關系,執行緒B中的變數也沒有依賴關系,所以執行緒A和執行緒B就有可能發生指令重排,會造成如下操作:
| 執行緒A | 執行緒B |
|---|---|
| b = 1 | a = 2 |
| x = a | y = b |
這時,得到的結果就為 x = 2 和 y = 1;
那么如何解決呢?
我們可以用volatile修飾,來避免指令重排,volatile會加一個記憶體屏障,來保證特定操作的執行順序以及記憶體可見性,

三、單例模式
1、常見的懶漢模式
這里我們跳過餓漢模式,直接上懶漢模式:
package com.zhan.juc.volatiletest.single;
/**
* 單例模式--懶漢
*
* @Author Zhanzhan
* @Date 2021/1/30 13:39
*/
public class LazyMan {
private static LazyMan lazyMan;
private LazyMan() {
System.out.println(Thread.currentThread().getName() + "ok");
}
/**
* 無檢測的單例模式
*
* @return
*/
public static LazyMan getInstanceByNotSafe() {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
return lazyMan;
}
public static void main(String[] args) {
/**
* 測驗多執行緒下,這種單例模式是否安全
*/
for (int i = 0; i < 10; i++) {
new Thread(() -> {
LazyMan.getInstanceByNotSafe();
}).start();
}
}
}
這樣寫對嗎?看結果:

我們發現不是單例的,那么一般的懶漢模式要怎么寫呢?
package com.zhan.juc.volatiletest.single;
/**
* 單例模式--懶漢
*
* @Author Zhanzhan
* @Date 2021/1/30 13:39
*/
public class LazyMan {
private static LazyMan lazyMan;
private LazyMan() {
System.out.println(Thread.currentThread().getName() + "ok");
}
/**
* 雙重檢測鎖 的 懶漢單例模式 DCL懶漢式
* @return
*/
public static LazyMan getInstance() {
if (lazyMan == null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
LazyMan.getInstance();
}).start();
}
}
}
看結果:

符合預期,是單例的,但是這里會有一個問題,new 單例物件的這個操作,不是一個原子操作,即 lazyMan = new LazyMan(); // 不是一個原子性操作
這一步經過了那幾個操作:
- 分配記憶體空間;
- 執行構造方法,初始化物件;
- 把這個物件指向這個空間,
我們正常認為的執行順序是 1=》2=》3
但是有可能會指令重排,執行的順序是 1=》3=》2
如果此時有A執行緒執行的順序是1=》3=》2,那么執行到3這一步時,有個B執行緒也進來了,就會發現lazyMan 這個物件不為null,于是直接回傳,但是此時lazyMan 還沒完成構造,就會引發問題,所以,就要使用volatile來修飾 lazyMan ,禁止指令重排,于是代碼如下:
package com.zhan.juc.volatiletest.single;
/**
* 單例模式--懶漢
*
* @Author Zhanzhan
* @Date 2021/1/30 13:39
*/
public class LazyMan {
private volatile static LazyMan lazyMan; // 使用volatile修飾,禁止指令重排
private LazyMan() {
System.out.println(Thread.currentThread().getName() + "ok");
}
/**
* 雙重檢測鎖 的 懶漢單例模式 DCL懶漢式
* @return
*/
public static LazyMan getInstance() {
if (lazyMan == null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
/**
* 1、分配記憶體空間;
* 2、執行構造方法,初始化物件;
* 3、把這個物件指向這個空間,
*/
lazyMan = new LazyMan(); // 不是一個原子性操作
}
}
}
return lazyMan;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
LazyMan.getInstance();
}).start();
}
}
}
2、破解一般的懶漢模式
我們一般就認為,上面的那種單例模式,沒啥毛病了,
但是!依然可以有方法破解上述的單例模式,怎么破解呢?用反射!
上代碼:
package com.zhan.juc.volatiletest.single;
import java.lang.reflect.Constructor;
/**
* 單例模式--懶漢
*
* @Author Zhanzhan
* @Date 2021/1/30 13:39
*/
public class LazyMan {
private volatile static LazyMan lazyMan; // 使用volatile修飾,禁止指令重排
private LazyMan() {
System.out.println(Thread.currentThread().getName() + "ok");
}
/**
* 雙重檢測鎖 的 懶漢單例模式 DCL懶漢式
* @return
*/
public static LazyMan getInstance() {
if (lazyMan == null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
/**
* 1、分配記憶體空間;
* 2、執行構造方法,初始化物件;
* 3、把這個物件指向這個空間,
*/
lazyMan = new LazyMan(); // 不是一個原子性操作
}
}
}
return lazyMan;
}
public static void main(String[] args) throws Exception {
// 用反射破解單例模式
LazyMan instance = LazyMan.getInstance();
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null); // 獲取構造器
declaredConstructor.setAccessible(true); // 無視私有構造器
LazyMan instance2 = declaredConstructor.newInstance(); // 通過反射創建物件
System.out.println(instance);
System.out.println(instance2);
}
}
看結果:

我們看到,這兩個物件實體的hashCode不一樣,是兩個不同的物件實體,于是,我們得出結論,一般的懶漢模式就這樣被反射破解了,那么有應對方法嗎?既然是獲取私有構造器,來創建實體,那么我們在私有構造器上也加上校驗呢?來看:
package com.zhan.juc.volatiletest.single;
import java.lang.reflect.Constructor;
/**
* 單例模式--懶漢
*
* @Author Zhanzhan
* @Date 2021/1/30 13:39
*/
public class LazyMan {
private volatile static LazyMan lazyMan; // 使用volatile修飾,禁止指令重排
private LazyMan() {
synchronized (LazyMan.class){
if (lazyMan != null){
throw new RuntimeException("不要試圖使用反射破壞例外");
}
}
System.out.println(Thread.currentThread().getName() + "ok");
}
/**
* 雙重檢測鎖 的 懶漢單例模式 DCL懶漢式
* @return
*/
public static LazyMan getInstance() {
if (lazyMan == null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
/**
* 1、分配記憶體空間;
* 2、執行構造方法,初始化物件;
* 3、把這個物件指向這個空間,
*/
lazyMan = new LazyMan(); // 不是一個原子性操作
}
}
}
return lazyMan;
}
public static void main(String[] args) throws Exception {
// 用反射破解單例模式
LazyMan instance = LazyMan.getInstance();
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null); // 獲取構造器
declaredConstructor.setAccessible(true); // 無視私有構造器
LazyMan instance2 = declaredConstructor.newInstance(); // 通過反射創建物件
System.out.println(instance);
System.out.println(instance2);
}
}
看結果:

我們發現,這種方法被抵擋了,那么,還有可能破解這種單例模式嗎?
我們思考下,剛才那種破解,第一個物件實體是通過單例模式來創建的,第二個物件實體是通過反射來創建的,那么如果兩個物件都通過反射來創建呢?上代碼:
package com.zhan.juc.volatiletest.single;
import java.lang.reflect.Constructor;
/**
* 單例模式--懶漢
*
* @Author Zhanzhan
* @Date 2021/1/30 13:39
*/
public class LazyMan {
private volatile static LazyMan lazyMan; // 使用volatile修飾,禁止指令重排
private LazyMan() {
synchronized (LazyMan.class){
if (lazyMan != null){
throw new RuntimeException("不要試圖使用反射破壞例外");
}
}
System.out.println(Thread.currentThread().getName() + "ok");
}
/**
* 雙重檢測鎖 的 懶漢單例模式 DCL懶漢式
* @return
*/
public static LazyMan getInstance() {
if (lazyMan == null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
/**
* 1、分配記憶體空間;
* 2、執行構造方法,初始化物件;
* 3、把這個物件指向這個空間,
*/
lazyMan = new LazyMan(); // 不是一個原子性操作
}
}
}
return lazyMan;
}
public static void main(String[] args) throws Exception {
// 用反射破解單例模式
// LazyMan instance = LazyMan.getInstance();
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null); // 獲取構造器
declaredConstructor.setAccessible(true); // 無視私有構造器
LazyMan instance2 = declaredConstructor.newInstance(); // 通過反射創建物件
LazyMan instance = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(instance2);
}
}
看結果:

非常的amazing啊,竟然又被破解了,既然這樣,我就較上勁兒了,還有什么方法能抵擋嗎?有的,我們用下紅綠燈模式,就是通過設定一個標識,來再次進行校驗,上代碼:
package com.zhan.juc.volatiletest.single;
import java.lang.reflect.Constructor;
/**
* 單例模式--懶漢
*
* @Author Zhanzhan
* @Date 2021/1/30 13:39
*/
public class LazyMan {
private static boolean flag = false;
private volatile static LazyMan lazyMan; // 使用volatile修飾,禁止指令重排
private LazyMan() {
synchronized (LazyMan.class){
if (!flag){
flag = true;
} else {
throw new RuntimeException("不要試圖使用反射破壞單例");
}
}
System.out.println(Thread.currentThread().getName() + "ok");
}
/**
* 雙重檢測鎖 的 懶漢單例模式 DCL懶漢式
* @return
*/
public static LazyMan getInstance() {
if (lazyMan == null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
/**
* 1、分配記憶體空間;
* 2、執行構造方法,初始化物件;
* 3、把這個物件指向這個空間,
*/
lazyMan = new LazyMan(); // 不是一個原子性操作
}
}
}
return lazyMan;
}
public static void main(String[] args) throws Exception {
// 用反射破解單例模式
// LazyMan instance = LazyMan.getInstance();
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null); // 獲取構造器
declaredConstructor.setAccessible(true); // 無視私有構造器
LazyMan instance2 = declaredConstructor.newInstance(); // 通過反射創建物件
LazyMan instance = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(instance2);
}
}
看結果:

符合預期,抵擋了這種方法,那到現在了,還有可能破解單例嗎?有的,就是,如果我知道了你設定的識別符號,我直接對識別符號進行修改呢?
package com.zhan.juc.volatiletest.single;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
/**
* 單例模式--懶漢
*
* @Author Zhanzhan
* @Date 2021/1/30 13:39
*/
public class LazyMan {
private static boolean flag = false;
private volatile static LazyMan lazyMan; // 使用volatile修飾,禁止指令重排
private LazyMan() {
synchronized (LazyMan.class){
if (!flag){
flag = true;
} else {
throw new RuntimeException("不要試圖使用反射破壞單例");
}
}
System.out.println(Thread.currentThread().getName() + "ok");
}
/**
* 雙重檢測鎖 的 懶漢單例模式 DCL懶漢式
* @return
*/
public static LazyMan getInstance() {
if (lazyMan == null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
/**
* 1、分配記憶體空間;
* 2、執行構造方法,初始化物件;
* 3、把這個物件指向這個空間,
*/
lazyMan = new LazyMan(); // 不是一個原子性操作
}
}
}
return lazyMan;
}
public static void main(String[] args) throws Exception {
// 用反射破解單例模式
// LazyMan instance = LazyMan.getInstance();
Field flag = LazyMan.class.getDeclaredField("flag");// 假設我們知道了識別符號的名稱
flag.setAccessible(true); // 無視私有權限
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null); // 獲取構造器
declaredConstructor.setAccessible(true); // 無視私有構造器
LazyMan instance2 = declaredConstructor.newInstance(); // 通過反射創建物件
// 更改識別符號
flag.set(instance2, false);
LazyMan instance = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(instance2);
}
}
看結果:

我們發現,又被破壞了,那么我們如何解決呢?看反射中newInstance()的原始碼:
/**
* Uses the constructor represented by this {@code Constructor} object to
* create and initialize a new instance of the constructor's
* declaring class, with the specified initialization parameters.
* Individual parameters are automatically unwrapped to match
* primitive formal parameters, and both primitive and reference
* parameters are subject to method invocation conversions as necessary.
*
* <p>If the number of formal parameters required by the underlying constructor
* is 0, the supplied {@code initargs} array may be of length 0 or null.
*
* <p>If the constructor's declaring class is an inner class in a
* non-static context, the first argument to the constructor needs
* to be the enclosing instance; see section 15.9.3 of
* <cite>The Java™ Language Specification</cite>.
*
* <p>If the required access and argument checks succeed and the
* instantiation will proceed, the constructor's declaring class
* is initialized if it has not already been initialized.
*
* <p>If the constructor completes normally, returns the newly
* created and initialized instance.
*
* @param initargs array of objects to be passed as arguments to
* the constructor call; values of primitive types are wrapped in
* a wrapper object of the appropriate type (e.g. a {@code float}
* in a {@link java.lang.Float Float})
*
* @return a new object created by calling the constructor
* this object represents
*
* @exception IllegalAccessException if this {@code Constructor} object
* is enforcing Java language access control and the underlying
* constructor is inaccessible.
* @exception IllegalArgumentException if the number of actual
* and formal parameters differ; if an unwrapping
* conversion for primitive arguments fails; or if,
* after possible unwrapping, a parameter value
* cannot be converted to the corresponding formal
* parameter type by a method invocation conversion; if
* this constructor pertains to an enum type.
* @exception InstantiationException if the class that declares the
* underlying constructor represents an abstract class.
* @exception InvocationTargetException if the underlying constructor
* throws an exception.
* @exception ExceptionInInitializerError if the initialization provoked
* by this method fails.
*/
@CallerSensitive
@ForceInline // to ensure Reflection.getCallerClass optimization
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
if (!override) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, clazz, modifiers);
}
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
我們看到:

3、使用列舉實作單例模式
上代碼:
package com.zhan.juc.volatiletest.single;
/**
* 列舉實作單例模式
* @Author Zhanzhan
* @Date 2021/1/30 14:34
*/
public class EnumSingle {
enum EnumTest{
INSTANCE;
private EnumSingle enumSingle = null;
private EnumTest(){
enumSingle = new EnumSingle();
}
public EnumSingle getEnumSingle(){
return enumSingle;
}
}
public static void main(String[] args){
EnumSingle instance1 = EnumTest.INSTANCE.getEnumSingle();
EnumSingle instance2 = EnumTest.INSTANCE.getEnumSingle();
System.out.println(instance1);
System.out.println(instance2);
}
}
看結果:

符合預期,是同一個物件,
看反射是否能破壞此單例模式:
package com.zhan.juc.volatiletest.single;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* 列舉實作單例模式
* @Author Zhanzhan
* @Date 2021/1/30 14:34
*/
public class EnumSingle {
enum EnumTest{
INSTANCE;
private EnumSingle enumSingle = null;
private EnumTest(){
enumSingle = new EnumSingle();
}
public EnumSingle getEnumSingle(){
return enumSingle;
}
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor<EnumTest> declaredConstructor = EnumTest.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
EnumTest instance1 = declaredConstructor.newInstance();
EnumTest instance2 = declaredConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
看結果:

符合預期,所以使用列舉實作單例模式,是最安全和簡單的,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/255153.html
標籤:其他
下一篇:3.3 執行緒安全分析
