發佈對象
使一個對象能夠被當前範圍之外的代碼所使用,將創建的對象保存到容器中,也可能通過某個方法返回
對象的引用,或者將引用傳遞到其他類的方法中
對象逸出
一種錯誤的發佈,當一個對象還沒有構造完成時,就使它被其他線程所見
1、發佈的對象只需要被它需要的線程被看見
2、避免對象逸出
發佈錯誤對象:
import java.util.Arrays;
//線程不安全的
//發佈對象
public class Student {
private String[] student = {"張三","李四","王五"};
public String[] getStudent(){
return student;
}
public static void main(String[] args) {
Student unsarePublish = new Student();
System.out.println(Arrays.toString(unsarePublish.getStudent()));
unsarePublish.getStudent()[1]="趙柳";
System.out.println(Arrays.toString(unsarePublish.getStudent()));
}
}
返回結果:
[張三, 李四, 王五]
[張三, 趙柳, 王五]
上述例子中我們可以看到,李四已經被趙柳替代,通過public 類的訪問級別,發佈了這些域,在外部都可以訪問這些域,這樣的發佈對象其實是不安全的,因爲無法假設其他線程會不會修改這個域,所以會導致student的值是不確定的,因此是線程不安全的。
如何來進行安全的發佈對象呢:
看了網上好多博客,這裏也整理了一下,我們就來使用最經典的,單例模式來設計
1 懶漢模式設計
1.1 懶漢模式
/**
* 懶漢模式
* 線程不安全
*/
public class LazyMode1 {
private LazyMode1(){//私有的構造函數}
private static LazyMode1 instance = null;//單例對象
//在單線程下是沒有問題的
public static LazyMode1 getInstance(){
if(instance == null){
instance = new LazyMode1();
}
return instance;
}
}
這是一個比較正常的單例模式的,是一個線程不安全的類,那麼如何讓他變成一個線程安全的類呢,看下面一個例子
1.2 synchronized
/**
* 懶漢模式
* 線程安全
*/
public class LazyMode2 {
private LazyMode2(){//私有的構造函數}
private static LazyMode2 instance = null; //單例對象
public synchronized static LazyMode2 getInstance(){
if(instance == null){
instance = new LazyMode2();
}
return instance;
}
添加了synchronized後,在同一時間,只能允許一個線程訪問,因此可以保證這個是線程安全的
雖然他是線程安全的,但是他帶來的性能上的開銷,而這個開銷是我們不希望的,不推薦使用,別擔心下面還有更好的,我們來看下面的知識點
1.3 雙重同步鎖
/**
* 懶漢模式
* 但是這個類並不是線程安全的類
*/
public class LazyMode3 {
private LazyMode3(){//私有的構造函數}
private static LazyMode3 instance = null; //單例對象
public static LazyMode3 getInstance(){
if(instance == null){
synchronized(LazyMode3.class){
if(instance == null){
instance = new LazyMode3();
}
}
}
return instance;
}
}
這個案例,我們使用了雙重同步鎖的單例模式,但是他並不是一個線程安全的類,因爲在JVM和cpu優化,發生了指令重排,在單線程下,是沒有影響的,但是在多線程下,就會打亂分配的內存空間和初始化對象的順序,就會導致我們的結果和預期的不一致,雖然這個發生的概率很小,但是會發生,所以他是線程不安全的類,那麼如何能夠讓他成爲一個線程安全的類呢,看下面的例子。
1.4 volatile+雙重同步鎖
/**
* 懶漢模式
* 線程安全
*/
public class LazyMode4 {
private LazyMode4(){//私有的構造函數}
private volatile static LazyMode4 instance = null;
public static LazyMode4 getInstance(){
if(instance == null){
synchronized(LazyMode4.class){
if(instance == null){
instance = new LazyMode4();
}
}
}
return instance;
}
}
在這裏呢,我們使用了volatile+雙重檢測機制 他可以禁止指令重排
爲什麼vloatle可以禁止指令重排?
1、通過加入內存屏障和禁止重排序優化來實現
2、對volatile變量寫操作時,會在寫操作後加入一條store屏障指令,講本地內存中的共享變量值刷新到主內存中
3、對volatile變量讀操作時,會在讀操作前加入一條load屏障指令,從主內存中讀取共享變量
2 餓漢模式設計
/**
* 餓漢模式
* 線程安全的
*/
public class HungryMode1 {
private HungryMode1(){//私有的構造函數}
private static HungryMode1 instance = new HungryMode1();//單例對象
public static HungryMode1 getInstance(){
return instance;
}
}
- 餓漢模式的不足:如果餓漢模式中存在過多的處理,會導致這個類在加載的時候特別慢,可能會引起性能問題,如果是隻進行類的加載,沒有實際的調用的話,會導致資源的浪費
- 所以我們需要考慮的一個點就是肯定會被使用,這樣纔不會導致過多的浪費,我們來看第二個例子
/**
* 餓漢模式
* 單例實例在類裝載時進行創建
*/
@ThreadSafe
public class HungryMode2 {
private HungryMode2(){//私有的構造函數}
//單例對象
private static HungryMode2 instance = null;
//靜態代碼塊
static {
instance = new HungryMode2();
}
public static HungryMode2 getInstance(){
return instance;
}
public static void main(String[] args) {
// System.out.println(getInstance());
// System.out.println(getInstance());
System.out.println(getInstance().hashCode());
System.out.println(getInstance().hashCode());
}
}
在這裏呢我們要注意的是:
當我們寫單例對象和靜態代碼塊的時候呢,一定要注意他們的順序,順序不一樣,執行結果會 有所不同
首先我們來看單例對象在靜態代碼塊前面
//單例對象
private static HungryMode2 instance = null;
//靜態代碼塊
static {
instance = new HungryMode2();
}
執行結果:
2061475679
2061475679
首先我們來看靜態代碼塊在單例對象前面
//靜態代碼塊
static {
instance = new HungryMode2();
}
//單例對象
private static HungryMode2 instance = null;
執行結果:
Exception in thread "main" java.lang.NullPointerException
at com.lyy.concurrency.singleton.HungryMode2.main(HungryMode2.java:37)
這裏會報空指針異常,這裏是爲什麼呢,是因爲如果靜態代碼塊在單例對象前面,會先執行靜態代碼塊,本來執行後已經有值了,但是到了下一步單例對象這裏,又會賦值爲空,所以就看看到我們執行的結果是空指針異常
我們可以debug看一下:
當我們執行的靜態代碼塊的時候是沒有值的
完成之後,我們發現instance是有值的,繼續往下走
到這裏後,我們會發現instance已經被設置成了Null
枚舉模式
/**
* 枚舉模式
*/
public class EnumModel1 {
//私有的構造函數
private EnumModel1(){
}
public static EnumModel1 getInstance(){
return Singleton.INSTANCE.getInstance();
}
private enum Singleton{
INSTANCE;
private EnumModel1 singleton;
//構造函數
Singleton(){
singleton = new EnumModel1();
}
public EnumModel1 getInstance(){
return singleton;
}
}
}
當我們通過枚舉來初始化這個對象的時候,JVM保證這個方法絕對只調用一次,並且是一個線程安全的類,不需要我們做過多的處理,使用起來也比較方便。所以我們推薦使用這種方式來進行安全發佈對象