java 静态代码块 多线程,Java多线程编程笔记10:单例模式
立即加載:“餓漢模式”
立即加載就是指使用類的時候已經將對象創建完畢,常見的實現方法就是直接new實例化。也就是在調用方法前,實例就被創建了。示例代碼如下所示:
class MyObject{
private static MyObject myObject=new MyObject();
private MyObject(){}
public static MyObject getInstance(){
//如果還有其他代碼,存在線程安全問題
return myObject;
}
}
class MyThread extends Thread{
@Override
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();
}
}
運行結果如下:
58615885
58615885
58615885
可以發現,實現了單例模式,因為多個線程得到的實例的hashCode是一樣的。
延遲加載:“懶漢模式”
延遲加載就是在調用getInstance()方法時實例才被創建,常見的方法就是在getInstance()方法中進行new實例化。實現代碼如下:
class MyObject{
private static MyObject myObject;
private MyObject(){}
public static MyObject getInstance(){
if(myObject==null){ //A線程執行
myObject=new MyObject();//B線程執行
}
return myObject;
}
}
class MyThread extends Thread{
@Override
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();
}
}
但是由于在getInstance()中,存在多條語句,因此可能存在線程安全問題。運行結果也顯示了這一點:
2041531420
1348345633
1348345633
甚至,當getInstance()中,有更多的語句,會出現不同的三個對象,在if(myObject==null)語句塊中加入Thread.sleep(3000),運行結果如下所示:
218620763
58615885
712355351
DCL
如果使用synchronized關鍵字,對整個getInstance()上鎖或者對整個if語句塊加鎖,會存在效率問題。
最終采用了DCL(Double-Check Locking)雙檢查鎖機制,也是大多數多線程結合單例模式使用的解決方案。第一層主要是為了避免不必要的同步,第二層判斷則是為了在null情況下才創建實例。
public class MyObject{
private static MyObject myObject;
private MyObject(){
}
public static MyObject getInstance(){
if (myObject == null) {//第一次檢查
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (MyObject.class) {//加鎖
if (myObject == null) {//第二次檢查
myObject = new MyObject();//創建對象
}
}
}
return myObject;
}
}
測試結果,得到的是相同的hashcode。
上述雙重檢查鎖定的方法從表面上看來達到了如下的效果:
多個線程試圖在同一時間創建對象時,通過加鎖保證只有一個線程能創建對象
在對象創建好之后,執行 getInstance() 方法將不需要獲取鎖,直接返回已經創建好的對象
創建對象可以分解為 分配內存;初始化;設置myObject指向的內存地址。但是由于重排序,可能導致先分配地址再進行初始化。因此,在線程A設置好了內存空間,還未初始化時,線程B判斷不為空,將訪問該對象,得到了一個還未初始化的對象。
解決辦法有兩種:
不允許初始化和設置變量指向的內存地址兩步重排序(使用volatile)
允許這兩步重排序,但是不允許其他線程看到這個重排序
基于volatile的解決方案
public class MyObject{
private volatile static MyObject myObject;
private MyObject(){
}
public static MyObject getInstance(){
if (myObject == null) {//第一次檢查
synchronized (MyObject.class) {//加鎖
if (myObject == null) {//第二次檢查
myObject = new MyObject();//創建對象
}
}
}
return myObject;
}
}
由于instance是volatile修飾的,初始化和設置內存地址在多線程環境中將被禁止重排序。
基于類初始化的解決方案(靜態內置類)
public class MyObject{
private static class MyObjectHandler{
private static MyObject myObject=new MyObject();
}
private MyObject(){
}
public static MyObject getInstance(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return MyObjectHandler.myObject;
}
}
采用靜態內置類的方法,是線程安全的。JVM在類的初始化階段,會執行類的初始化。在執行類的初始化期間,JVM會去獲取一個鎖,可以同步多個線程對同一個類的初始化。雖然可能重排序,但是其他線程是無法看到這個過程的。
類的初始化過程:
通過在 Class 對象上同步(獲取 Class 對象的初始化鎖),控制類或接口的初始化。這個獲取鎖的線程會一直等待,直到當前線程能獲得這個初始化鎖。
線程A執行類的初始化,線程B在初始化鎖對應的 condition 上等待。
線程A設置state=initialized,然后喚醒在 condition 中等待的所有線程。
其他線程獲取到初始化鎖,讀取到state,釋放該鎖,然后類初始化處理過程完成。
使用static代碼塊
靜態代碼塊的代碼再使用類的時候就已經執行了,所以可以應用靜態代碼塊的這個特性來實現單例設計模式。
public class MyObject{
private static MyObject myObject=null;
static{myObject=new MyObject();}
private MyObject(){
}
public static MyObject getInstance(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return myObject;
}
}
使用enum枚舉數據類型
使用枚舉類時,和靜態代碼塊的特性相似,構造方法會被自動調用。枚舉在經過javac的編譯之后,會被轉換成形如public final class T extends Enum的定義。也就是說,我們定義的一個枚舉,在第一次被真正用到的時候,會被虛擬機加載并初始化,而這個初始化過程是線程安全的。而我們知道,解決單例的并發問題,主要解決的就是初始化過程中的線程安全問題。
所以,由于枚舉的以上特性,枚舉實現的單例是天生線程安全的。同時,枚舉可解決反序列化會破壞單例的問題。
enum MyObject{
INSTANCE;
}
SimpleDataFormat
SimpleDataFormat使用了單例模式,具有線程安全問題。SimpleDateFormat中的日期格式不是同步的。推薦(建議)為每個線程創建獨立的格式實例。如果多個線程同時訪問一個格式,則它必須保持外部同步。
解決方案1:需要的時候創建新實例
public class DateUtil {
public static String formatDate(Date date)throws ParseException{
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.format(date);
}
public static Date parse(String strDate) throws ParseException{
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.parse(strDate);
}
}
在需要用到SimpleDateFormat 的地方新建一個實例,不管什么時候,將有線程安全問題的對象由共享變為局部私有都能避免多線程問題,不過也加重了創建對象的負擔。在一般情況下,這樣其實對性能影響比不是很明顯的。
解決方案2:同步SimpleDateFormat對象
public class DateSyncUtil{
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static String formatDate(Date date)throws ParseException{
synchronized(sdf){
return sdf.format(date);
}
}
public static Date parse(String strDate) throws ParseException{
synchronized(sdf){
return sdf.parse(strDate);
}
}
}
當線程較多時,當一個線程調用該方法時,其他想要調用此方法的線程就要block,多線程并發量大的時候會對性能有一定的影響。
解決方案3:使用ThreadLocal
public class ConcurrentDateUtil{
private static ThreadLocal threadLocal = new ThreadLocal() {
@Override
protected DateFormat initialValue(){
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public static Date parse(String dateStr) throws ParseException{
return threadLocal.get().parse(dateStr);
}
public static String format(Date date){
return threadLocal.get().format(date);
}
}
使用ThreadLocal, 也是將共享變量變為獨享,線程獨享肯定能比方法獨享在并發環境中能減少不少創建對象的開銷。如果對性能要求比較高的情況下,一般推薦使用這種方法。
總結
以上是生活随笔為你收集整理的java 静态代码块 多线程,Java多线程编程笔记10:单例模式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: php 自加 性能,对于数据库的自增、自
- 下一篇: java和内存交互,java内存模型-内