日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

单例模式在多线程中的安全性研究

發布時間:2025/3/20 编程问答 42 豆豆
生活随笔 收集整理的這篇文章主要介紹了 单例模式在多线程中的安全性研究 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

概述

關于一般單例模式的創建和分析在我的另一篇博客《Java設計模式——單件模式》中有詳細說明。只是在上篇博客中的單例是針對于單線程的操作,而對于多線程卻并不適用,本文就從單例模式與多線程安全的角度出發,講解單例模式在多線程中應該如何被使用。


版權說明

著作權歸作者所有。
商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
本文作者:Coding-Naga
發表日期: 2016年4月6日
本文鏈接:http://blog.csdn.net/lemon_tree12138/article/details/51074383
來源:CSDN
更多內容:分類 >> 并發與多線程


目錄

文章目錄

  • 概述
  • 版權說明
  • 目錄
    • @[toc]
  • 一般情況下的單例模式的創建
  • 基于 synchronized 的同步解決方案
  • 基于雙重檢查鎖定的解決方案
    • 方案分析及測試
    • 存在的問題
  • 基于 volatile 的解決方案
  • 基于類初始化的解決方案
  • 基于枚舉的解決方案
  • Ref
  • 征集

一般情況下的單例模式的創建

首先我們基于單例模式來編寫一個Student的類。如下:
Student.java

public class Student {private static Student student = null;private Student() {}public static Student getInstance() {if (student == null) {System.out.println("線程" + Thread.currentThread() + "進入,student = " + student);student = new Student();}return student;} }

我們將創建學生類的任務交給一個 Runnable 去完成。
CreateRunnable.java

public class Createable implements Runnable {@Overridepublic void run() {Student student = Student.getInstance();System.out.println("學生類被創建:" + student);System.out.println("Hashcode:" + student.hashCode());}}

如下是測試代碼:
Client.java

public class Client {public static void main(String[] args) {Thread thread1 = new Thread(new Createable());Thread thread2 = new Thread(new Createable());thread1.start();thread2.start();} }

運行結果

線程Thread[Thread-0,5,main]進入,student = null 線程Thread[Thread-1,5,main]進入,student = null 學生類被創建:org.naga.demo.thread.singleton.Student@1dbaf954 學生類被創建:org.naga.demo.thread.singleton.Student@6df90bbf Hashcode:1845038015 Hashcode:498792788

從上面程序的運行結果來看,很明顯這里創建了兩個不同的對象。這與單例模式的定義相悖了。因為在多線程環境下,很明顯 getInstance() 方法不能保證原子性,所以這種方法在多線程下是不安全的。


基于 synchronized 的同步解決方案

在一般情況下的單例模式的創建中,我們知道那是一種不安全的創建對象的方案。那么就很容易想到用多線程同步的方法來解決,就是使用關鍵字 synchronized 來實現同步策略。使用 synchronized 之后的代碼及運行結果如下:
Student.java

public synchronized static Student getInstance() {if (student == null) {System.out.println("線程" + Thread.currentThread() + "進入,student = " + student);student = new Student();}return student;}

運行結果

線程Thread[Thread-0,5,main]進入,student = null 學生類被創建:org.naga.demo.thread.singleton.Student@1dbaf954 Hashcode:498792788 學生類被創建:org.naga.demo.thread.singleton.Student@1dbaf954 Hashcode:498792788

從運行結果上可以看出,這里的同步策略是有效的,Thread-0 和 Thread-1 創建的是同一個對象。而關于 synchronized 關鍵字的詳細說明請參見《Java多線程之synchronized和volatile的比較》一文。
不過,對于系統而言,synchronized 同步策略的實現其實是一項性能開銷非常大的操作。這可能是 synchronized 需要對對象加鎖的緣故。


基于雙重檢查鎖定的解決方案

方案分析及測試

上面說到 synchronized 同步策略對性能開銷比較大,對于可能存在大量的 getInstance() 方法調用時,對于系統而言可能就會難以負荷或運行緩慢。這里想到的方法就是減少對 synchronized 關鍵字的調用。也就是下面要說的雙重檢查鎖定。
Student.java

public class Student {... ... public static Student getInstance() {System.out.println("線程" + Thread.currentThread() + "進入,student = " + student);if (student == null) {synchronized(Student.class) {if (student == null) {student = new Student();}}}return student;} }

運行結果-1

線程Thread[Thread-0,5,main]進入,student = null 學生類被創建:org.naga.demo.thread.singleton.Student@386f4317 學生類被創建:org.naga.demo.thread.singleton.Student@386f4317 Hashcode:946815767 Hashcode:946815767

運行結果-2

線程Thread[Thread-1,5,main]進入,student = null 學生類被創建:org.naga.demo.thread.singleton.Student@1dbaf954 Hashcode:498792788 線程Thread[Thread-0,5,main]進入,student = org.naga.demo.thread.singleton.Student@1dbaf954 學生類被創建:org.naga.demo.thread.singleton.Student@1dbaf954 Hashcode:498792788

這里的結果是沒有問題的。只是你可能會有疑問,為什么這里采用雙重檢查鎖定?之前我們不是已經對 student 對象進行了判空操作了么,這里怎么還要進行第二次判空?其實在理解了多線程執行的過程,這個問題也就很好回答了。假定有兩個線程 T-0 和 T-1,它們現在同時到達第一個 if (student == null) 判空操作,那么這兩個線程都可以進入到 if (student == null) 的內部,因為在此之前對象的訪問還沒有被鎖定;這個時候,如果 T-0 獲得了鎖,并對對象進行初始化操作,結束后釋放鎖;然后 T-1 獲得了 T-0 釋放的鎖,如果這里不進行第二次判空操作的話,那么 T-1 也會創建一個對象,這個對象與 T-0 創建的是兩個完全不同的對象。而如果這里我們進行了第二次判空操作,那么 T-1 得到的對象不為空,就不會再次創建新的對象了。這個方案設計得十分巧妙,既解決了同步帶來的性能開銷,又保證了單例模式的構建。

存在的問題

對于這一小節,我本人還沒有找到一個可以正確測試的方法。這里所作的邏輯說明是來自于《Java 并發編程的藝術》一書。如果你有好的驗證方法,歡迎以評論的方式與我交流,共同進步。
這里介紹的雙重檢查鎖定的方案,這的確是一個很巧妙的設計。不過也存在一些細微的問題,這個問題就在于 student = new Student(); 這句代碼。對于通過 new 創建對象的過程可以分解成以下3行偽代碼。

memory = allocate(); // 1: 分配對象的內存空間 ctorInstance(memory); // 2: 初始化對象 instance = memory; // 3: 設置 instance 指向剛分配的內存地址

而這里的2、3兩個步驟可以被重排序,重排序的結果就像下面的這樣:

memory = allocate(); // 1: 分配對象的內存空間 instance = memory; // 2: 設置 instance 指向剛分配的內存地址// 這時,memory處的對象還沒有被初始化 ctorInstance(memory); // 3: 初始化對象

因為這個重排序的過程,所以這里就有一個問題了。假設有一個線程 T-0 當前執行到上面重排序后偽代碼的第2步完成,第3步還沒開始時,有一個線程 T-1 進來了,要進行第一次 if (student == null) 判斷。因為這里 instance 已經被指向了 memory 分配的地址了。所以,這時 T-1 判斷的對象是一個未被初始化的對象。這樣就出現了下面這樣的輸出了。

線程Thread[Thread-1,5,main]進入,student = null 學生類被創建:org.naga.demo.thread.singleton.Student@1dbaf954 Hashcode:498792788 線程Thread[Thread-0,5,main]進入,student = org.naga.demo.thread.singleton.Student@1dbaf954 學生類被創建:org.naga.demo.thread.singleton.Student@1dbaf954 Hashcode:498792788

盡管如此,我們還是不能直接就說出了問題,因為這里也有可能 T-1 就是在 T-0 對對象創建完成之后才進來的。這里還是看看最佳的實踐方案吧。


基于 volatile 的解決方案

上面介紹了雙重檢查鎖定存在的一些弊端,不過我們還是有辦法解決的。只要對 student 對象進行 volatile 關鍵字修飾即可。
Student.java

public class Student {private volatile static Student student = null;... ... }

運行結果

線程Thread[Thread-0,5,main]進入,student = null 學生類被創建:org.naga.demo.thread.singleton.Student@6df90bbf 學生類被創建:org.naga.demo.thread.singleton.Student@6df90bbf Hashcode:1845038015 Hashcode:1845038015

這樣就保證了多線程之間,對共享變量的可見性。


基于類初始化的解決方案

在類的初始化階段(即在Class被加載之后,且被線程使用之前),會執行類的初始化。在執行類的初始化期間,JVM會去獲得一個鎖。這個鎖可以同步多個線程對同一個類的同步初始化。

Student.java

public class Student {private Student() {}private static class StudentHolder {private final static Student instance = new Student();}public static Student getInstance() {return StudentHolder.instance;} }

運行結果

學生類被創建:org.naga.demo.thread.singleton.Student@386f4317 學生類被創建:org.naga.demo.thread.singleton.Student@386f4317 Hashcode:946815767 Hashcode:946815767

基于枚舉的解決方案

說到了這里,其實我們還是有一個 bigger 更高的解決方案。那就是使用枚舉,使用枚舉的好處在于我們不用關心它是否安全,是否真是只有一個實例。下面是采用單例的一些好處:

  • 自由序列化;
  • 保證只有一個實例(即使使用反射機制也無法多次實例化一個枚舉量);
  • 線程安全。
  • Student.java

    public enum Student {INSTANCE;private String name;public String getName() {return name;}public void setName(String name) {this.name = name;} }

    Createable.java

    public class Createable implements Runnable {@Overridepublic void run() {Student student = Student.INSTANCE;System.out.println("學生類被創建:" + student);System.out.println("Hashcode:" + student.hashCode());}}

    運行結果

    學生類被創建:INSTANCE 學生類被創建:INSTANCE Hashcode:1946798030 Hashcode:1946798030

    使用枚舉除了線程安全和防止反射強行調用構造器之外,還提供了自動序列化機制,防止反序列化的時候創建新的對象。因此,Effective Java推薦盡可能地使用枚舉來實現單例。


    Ref

    • 《Java 多線程編程核心技術》
    • 《Java 并發編程的藝術》

    征集

    如果你也需要使用ProcessOn這款在線繪圖工具,可以使用如下邀請鏈接進行注冊:
    https://www.processon.com/i/56205c2ee4b0f6ed10838a6d

    總結

    以上是生活随笔為你收集整理的单例模式在多线程中的安全性研究的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。