如何保證單例模式在多線程中的線程安全性
對大數據、分布式、高並發等知識的學習必須要有多線程的基礎。這里討論一下如何在多線程的情況下設計單例模式。在23中設計模式中單例模式是比較常見的,在非多線程的情況下寫單例模式,考慮的東西會很少,但是如果將多線程和單例模式結合起來,考慮的事情就變多了,如果使用不當(特別是在生成環境中)就會造成嚴重的后果。所以如何使單例模式在多線程中是安全的顯得尤為重要,下面介紹各個方式的優缺點以及可用性:
1.立即加載(餓漢模式)
立即加載模式就是在調用getInstance()方法前,實例就被創建了,例:
public class MyObject {
// 立即加載方式 ==餓漢模式
private static MyObject myObject=new MyObject();
private MyObject(){
}
public static MyObject getInstance(){
return myObject;
}
}
-------------------------------------------------------------------
public class MyThread extends Thread{
public void run(){
System.out.println(MyObject.getInstance().hashCode());
}
}
------------------------------------------------------------------
public class Run {
public static void main(String[] args) {
MyThread t1=new MyThread();
MyThread t2=new MyThread();
MyThread t3=new MyThread();
t1.start();
t2.start();
t3.start();
}
}
控制台打印:
714682869
714682869
714682869
控制台打印出3個相同的hashCode,說明只有一個對象,這就是立即加載的單例模式。但是這種模式有一個缺點,就是不能有其他的實例變量,因為getInstance()方法沒有同步,所以可能出現非線程安全問題。
2.延遲加載(懶漢模式)
延遲加載就是在getInstance()方法中創建實例,例:
public class MyObject {
private static MyObject myObject;
private MyObject(){
}
public static MyObject getInstance(){
// 延遲加載
if(myObject!=null){
}else{
myObject=new MyObject();
}
return myObject;
}
}
-------------------------------------------------------------------
public class MyThread extends Thread{
public void run(){
System.out.println(MyObject.getInstance().hashCode());
}
}
-------------------------------------------------------------------
public class Run {
public static void main(String[] args) {
MyThread t1=new MyThread();
t1.start();
}
}
控制台打印:
1701381926
控制台打印出一個實例。缺點:在多線程的環境中,就會出現取多個實例的情況,與單例模式的初衷相背離。所以在多線程的環境中,此實例代碼是錯誤的。
3.延遲加載中使用synchronized修飾方法
public class MyObject {
private static MyObject myObject;
private MyObject(){
}
synchronized public static MyObject getInstance(){
try {
if(myObject!=null){
}else{
Thread.sleep(3000);
myObject=new MyObject();
}
} catch (InterruptedException e) {
// TODO: handle exception
e.printStackTrace();
}
return myObject;
}
}
-------------------------------------------------------------------
public class MyThread extends Thread{
public void run(){
System.out.println(MyObject.getInstance().hashCode());
}
}
-------------------------------------------------------------------
public class Run {
public static void main(String[] args) {
MyThread t1=new MyThread();
MyThread t2=new MyThread();
MyThread t3=new MyThread();
t1.start();
t2.start();
t3.start();
}
}
控制台打印:
1069480624
1069480624
1069480624
雖然得到了相同的實例,但是我們知道synchronized是同步的,一個線程必須等待另一個線程釋放鎖之后才能執行,影響了效率。
4.延遲加載中使用同步代碼塊,對類加鎖
public class MyObject {
private static MyObject myObject;
private MyObject(){
}
public static MyObject getInstance(){
try {
synchronized(MyObject.class){
if(myObject!=null){
}else{
Thread.sleep(3000);
myObject=new MyObject();
}
}
} catch (InterruptedException e) {
// TODO: handle exception
e.printStackTrace();
}
return myObject;
}
}
-------------------------------------------------------------------
public class MyThread extends Thread {
public void run(){
System.out.println(MyObject.getInstance().hashCode());
}
}
-------------------------------------------------------------------
public class Run {
public static void main(String[] args) {
MyThread t1=new MyThread();
MyThread t2=new MyThread();
MyThread t3=new MyThread();
t1.start();
t2.start();
t3.start();
}
}
控制台打印:
1743911840
1743911840
1743911840
此代碼雖然是正確的,但getInstance()方法里的代碼都是同步的了,其實也和第三種方式一樣會降低效率
5.使用DCL雙檢查鎖機制
DCL雙檢查鎖機制即使用volatile關鍵字(使變量在多個線程中可見)修改對象和synchronized代碼塊
public class MyObject {
private volatile static MyObject myObject;
private MyObject(){
}
public static MyObject getInstance(){
try {
if(myObject!=null){
}else{
Thread.sleep(3000);
synchronized(MyObject.class){
if(myObject==null){
myObject=new MyObject();
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
// TODO: handle exception
}
return myObject;
}
}
-------------------------------------------------------------------
public class MyThread extends Thread {
public void run(){
System.out.println(MyObject.getInstance().hashCode());
}
}
-------------------------------------------------------------------
public class Run {
public static void main(String[] args) {
MyThread t1=new MyThread();
MyThread t2=new MyThread();
MyThread t3=new MyThread();
t1.start();
t2.start();
t3.start();
}
}
控制台打印:
798941612
798941612
798941612
使用DCL雙檢查鎖機制,成功解決了延遲加載模式中遇到的多線程問題,實現了線程安全。其實大多數多線程結合單例模式情況下使用DCL是一種好的解決方案。
6.使用靜態內置類實現單例模式
public class MyObject {
// 內部類方式
private static class MyObjectHandler{
private static MyObject myObject=new MyObject();
}
private MyObject(){
}
public static MyObject getInstance(){
return MyObjectHandler.myObject;
}
}
-------------------------------------------------------------------
public class MyThread extends Thread {
public void run(){
System.out.println(MyObject.getInstance().hashCode());
}
}
-------------------------------------------------------------------
public class Run {
public static void main(String[] args) {
MyThread t1=new MyThread();
MyThread t2=new MyThread();
MyThread t3=new MyThread();
t1.start();
t2.start();
t3.start();
}
}
控制台打印:
1743911840
1743911840
1743911840
使用靜態內置類可以解決多線程中單例模式的非線程安全的問題,實現線程安全,但是如果對象是序列化的就無法達到效果了。
7.序列化與反序列化的單例模式
需要readResolve方法
public class MyObject implements Serializable{
private static final long serialVersionUID=888L;
// 內部類
private static class MyObjectHandler{
private static final MyObject myObject=new MyObject();
}
private MyObject(){
}
public static MyObject getInstance(){
return MyObjectHandler.myObject;
}
protected Object readResolve() throws ObjectStreamException {
System.out.println("調用了readResolve方法");
return MyObjectHandler.myObject;
}
}
-------------------------------------------------------------------
public class SaveAndRead {
public static void main(String[] args) {
try {
MyObject myObject=MyObject.getInstance();
FileOutputStream fosRef=new FileOutputStream(new File("myObjectFile.txt"));
ObjectOutputStream oosRef=new ObjectOutputStream(fosRef);
oosRef.writeObject(myObject);
oosRef.close();
fosRef.close();
System.out.println(myObject.hashCode());
} catch (FileNotFoundException e) {
// TODO: handle exception
} catch(IOException e){
e.printStackTrace();
}
try {
FileInputStream fisRef=new FileInputStream(new File("myObjectFile.txt"));
ObjectInputStream iosRef=new ObjectInputStream(fisRef);
MyObject myObject=(MyObject) iosRef.readObject();
iosRef.close();
fisRef.close();
System.out.println(myObject.hashCode());
} catch (FileNotFoundException e) {
// TODO: handle exception
} catch(IOException e){
e.printStackTrace();
} catch(ClassNotFoundException e){
e.printStackTrace();
}
}
}
控制台打印:
1988716027
調用了readResolve方法
1988716027
調用了readResolve方法后就是單例了,如果我們注釋掉readResolve方法,
控制台打印:
977199748
536468534
8.使用static代碼塊實現單例模式
public class MyObject {
private static MyObject instance=null;
private MyObject(){
}
static {
instance=new MyObject();
}
public static MyObject getInstance(){
return instance;
}
}
-------------------------------------------------------------------
public class MyThread extends Thread{
public void run(){
for (int i = 0; i <5; i++) {
System.out.println(MyObject.getInstance().hashCode());
}
}
}
-------------------------------------------------------------------
public class Run {
public static void main(String[] args) {
MyThread t1=new MyThread();
MyThread t2=new MyThread();
MyThread t3=new MyThread();
t1.start();
t2.start();
t3.start();
}
}
控制台打印:
798941612
798941612
798941612
https://blog.csdn.net/gan785160627/article/details/81946242
應用單例模式時,類只能有一個對象實例,這么做的目的是避免不一致狀態。
餓漢式單例:(立即加載)
-
// 餓漢式單例
-
public class Singleton1 {
-
-
// 指向自己實例的私有靜態引用,主動創建
-
private static Singleton1 singleton1 = new Singleton1();
-
-
// 私有的構造方法
-
private Singleton1(){}
-
-
// 以自己實例為返回值的靜態的公有方法,靜態工廠方法
-
public static Singleton1 getSingleton1(){
-
return singleton1;
-
}
-
}
懶漢式單例:(延遲加載)
-
// 懶漢式單例
-
public class Singleton2 {
-
-
// 指向自己實例的私有靜態引用
-
private static Singleton2 singleton2;
-
-
// 私有的構造方法
-
private Singleton2(){}
-
-
// 以自己實例為返回值的靜態的公有方法,靜態工廠方法
-
public static Singleton2 getSingleton2(){
-
// 被動創建,在真正需要使用時才去創建
-
if (singleton2 == null) {
-
singleton2 = new Singleton2();
-
}
-
return singleton2;
-
}
-
}
多線程下線程安全的懶漢式單例(餓漢式本身是線程安全的):
1)、同步延遲加載 — synchronized方法
-
// 線程安全的懶漢式單例
-
public class Singleton2 {
-
-
private static Singleton2 singleton2;
-
-
private Singleton2(){}
-
-
// 使用 synchronized 修飾,臨界資源的同步互斥訪問
-
public static synchronized Singleton2 getSingleton2(){
-
if (singleton2 == null) {
-
singleton2 = new Singleton2();
-
}
-
return singleton2;
-
}
-
}
2)、同步延遲加載 — synchronized塊
-
// 線程安全的懶漢式單例
-
public class Singleton2 {
-
-
private static Singleton2 singleton2;
-
-
private Singleton2(){}
-
-
-
public static Singleton2 getSingleton2(){
-
synchronized(Singleton2.class){ // 使用 synchronized 塊,臨界資源的同步互斥訪問
-
if (singleton2 == null) {
-
singleton2 = new Singleton2();
-
}
-
}
-
return singleton2;
-
}
-
}
3)、同步延遲加載 — 使用內部類實現延遲加載
-
// 線程安全的懶漢式單例
-
public class Singleton5 {
-
-
// 私有內部類,按需加載,用時加載,也就是延遲加載
-
private static class Holder {
-
private static Singleton5 singleton5 = new Singleton5();
-
}
-
-
private Singleton5() {
-
-
}
-
-
public static Singleton5 getSingleton5() {
-
return Holder.singleton5;
-
}
-
}
4)雙重檢測
-
// 線程安全的懶漢式單例
-
public class Singleton3 {
-
-
//使用volatile關鍵字防止重排序,因為 new Instance()是一個非原子操作,可能創建一個不完整的實例
-
private static volatile Singleton3 singleton3;
-
-
private Singleton3() {
-
}
-
-
public static Singleton3 getSingleton3() {
-
// Double-Check idiom
-
if (singleton3 == null) {
-
synchronized (Singleton3.class) { // 1
-
// 只需在第一次創建實例時才同步
-
if (singleton3 == null) { // 2
-
singleton3 = new Singleton3(); // 3
-
}
-
}
-
}
-
return singleton3;
-
}
-
}
5)ThreadLocal
-
public class Singleton {
-
-
// ThreadLocal 線程局部變量,將單例instance線程私有化
-
private static ThreadLocal<Singleton> threadlocal = new ThreadLocal<Singleton>();
-
private static Singleton instance;
-
-
private Singleton() {
-
-
}
-
-
public static Singleton getInstance() {
-
-
// 第一次檢查:若線程第一次訪問,則進入if語句塊;否則,若線程已經訪問過,則直接返回ThreadLocal中的值
-
if (threadlocal.get() == null) {
-
synchronized (Singleton.class) {
-
if (instance == null) { // 第二次檢查:該單例是否被創建
-
instance = new Singleton();
-
}
-
}
-
threadlocal. set(instance); // 將單例放入ThreadLocal中
-
}
-
return threadlocal.get();
-
}
-
}