前言
繼續(xù)學(xué)習(xí)Java多線程基礎(chǔ)與使用詳細(xì)篇(四)----Java內(nèi)存模型下的知識。本篇會涉及volatile關(guān)鍵字以及單例模式采章。
1. volatile 是什么
(1).volatile是一種同步機制,比synchronized或者Lock相關(guān)類更輕量,因為使用volatile并不會發(fā)生上下文切換等開銷很大的行為瘦馍。
(2).如果一個變量別修飾成volatile,那么JVM就知道了這個變量可能會被并發(fā)修改。
(3).但是開銷小,相應(yīng)的能力也小逛薇,雖然說volatile是用來同步的保證線程安全的,但是volatile做不到synchronized那樣的原子保護, volatile僅在很有限的場景下才能發(fā)揮作用惠桃。
2.volatile 適用場景
2.1 不適用: a++
代碼演示:
與AtomicInteger 相比下,
打印出的值沒有到預(yù)期的結(jié)果惰爬,只有AtomicInteger 是預(yù)期的喊暖,
所以a ++的時候沒有起到原子保護
public class NoVolatile implements Runnable {
volatile int a;
AtomicInteger realA = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
Runnable r = new NoVolatile();
Thread thread1 = new Thread(r);
Thread thread2 = new Thread(r);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(((NoVolatile) r).a);
System.out.println(((NoVolatile) r).realA.get());
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
a++;
realA.incrementAndGet();
}
}
}
18909
20000
2.2 不適用場合2
在下面適用了 done = !done;,在前面運行的時候有起到原子保護作用一直是flase撕瞧,
但是在多次運行之后就是true了陵叽,因此這樣是不適應(yīng)的場景。
public class NoVolatile2 implements Runnable {
volatile boolean done = false;
AtomicInteger realA = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
Runnable r = new NoVolatile2();
Thread thread1 = new Thread(r);
Thread thread2 = new Thread(r);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(((NoVolatile2) r).done);
System.out.println(((NoVolatile2) r).realA.get());
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
flipDone();
realA.incrementAndGet();
}
}
private void flipDone() {
done = !done;
}
}
2.3 適用場合1
適用 boolean flag,如果一個共享變量自始至終只被
各個線程賦值丛版,而沒有其他的操作巩掺,那么就可以用volatile來代替
synchronized或代替原子變量,因為賦值自身是有原子性的页畦,而volatile
又保證了可見性胖替,所以就足以保證線程安全。
public class UseVolatile1 implements Runnable {
volatile boolean done = false;
AtomicInteger realA = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
Runnable r = new UseVolatile1();
Thread thread1 = new Thread(r);
Thread thread2 = new Thread(r);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(((UseVolatile1) r).done);
System.out.println(((UseVolatile1) r).realA.get());
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
setDone();
realA.incrementAndGet();
}
}
private void setDone() {
done = true;
}
}
true
20000
2.4 適用場合2
作為刷新之前變量的觸發(fā)器
// 例如聲明一個 volatile 關(guān)鍵字
volatile boolean flag = false豫缨;
.....
// Thread A
.....
flag = true // 賦值為true
....
// Thread B
if(!flag){ //此時已經(jīng)刷新了独令,被線程B完全的看到了
}
3. volatile的作用: 可見性、禁止重排序
3.1. 可見性:讀一個volatile變量之前好芭,需要先使相應(yīng)的本地緩存失效燃箭,這樣就必須到主內(nèi)存讀取最新值,寫一個volatile 屬性會立即刷入到主內(nèi)存
3.2. 禁止指令重排序優(yōu)化:解決單例雙重鎖亂序的問題
4.volatile和synchronized的關(guān)系
volatile在這方面可以看做是輕量版的synchronized:如果一個共享變量
自始至終只被各個線程賦值舍败,而沒有其他的操作招狸,那么就可以用
volatile來代替synchronized或者代替原子變量,因為賦值自身是有原
子性的 ,而volatile又保證了可見性邻薯,所以就足以保證線程安全裙戏。
5. volatile 學(xué)習(xí)小結(jié)
(1).volatile修飾符適用于以下場景:某個屬性被多個線程共享,其中
有一個線程修改了此屬性厕诡,其他線程可以立即得到修改后的值累榜,比如
Boolean flag ; 或者作為觸發(fā)器,實現(xiàn)輕量級同步
(2). volatile屬性的讀寫操作都是無鎖的木人,它不能替代synchronized
因為它沒有提供原子性和互斥性信柿。因為無鎖,不需要花費時間在獲取鎖和
釋放鎖上醒第,所以說它是低成本的渔嚷。
(3). volatile只能作用于屬性,我們用volatile修飾屬性稠曼,這樣compilers就不會對這個屬性做指令重排序形病。
(4).volatile提供了可見性,任何一個線程對其的修改將立馬對其他線程可見。volatile 屬性不會被線程緩存漠吻,始終從主存中讀取量瓜。
(5).volatile 提供了happens-before保證,對volatile變量v的寫入happens-before所有其他線程后續(xù)對v的讀操作途乃。
(6).volatile可以使得long和double的賦值是原子的绍傲,后面馬上會講long和double的原子性
6.能保證可見性的措施
(1).除了 volatile可以讓變量保證可見性外,synchronized耍共、lock烫饼、并發(fā)集合、
Thread.join()和Thread.start()等都可以保證可見性
(2). 具體看happens-before 原則的規(guī)定
(3). 升華:對 synchronized 可見性正確理解
synchronized不僅保證了原子性试读、還保證了可見性
synchronized不僅被保護的代碼安全杠纵,還近朱者赤
演示假設(shè)演示:
// 假設(shè) 聲明 a b c
int a = 1 ;
int b = 3 钩骇;
int c = 2 比藻;
void change(){
a = 3;
b = 4;
sysnchronized(this){ // a,b 發(fā)生在sysnchronized 解鎖之前倘屹,第一個線程進入到sysnchronized內(nèi)后就進行解鎖。
c = 5;
}
void prinft(){
sysnchronized(this){
int a1 = a; // 利用sysnchronized happen-before 原則
// 第二個線程進到這里的時候它就可看到之前所有的變化纽匙,包括a,b內(nèi)的值
}
int b2 = b;
int c2 = c;
}
}
7.原子性
什么是原子性
(1). 一系列的操作,要么全部執(zhí)行成功哄辣,要么全部不執(zhí)行赠尾,不會出現(xiàn)執(zhí)行一般的情況,是不可分割气嫁。
(2). 例如ATM里取錢這樣的例子
(3). i++ 不是原子性
假設(shè)從上面的圖可以看到兩個線程
//假設(shè)當(dāng)線程一的
i = 1時,
//執(zhí)行
i = i +1 寸宵,
//最終結(jié)果
i = 2,
但是在線程二的時候沒讀到或者沒有看到梯影,它是看到i=1巫员,所以就不是原子性的
7. 1用synchronized 實現(xiàn)原子性
由于上面 i ++ 導(dǎo)致不是原子性的問題,可以使用synchronized保證同時只有一個線程運行
,這樣就是實現(xiàn)了原子性操作甲棍。
//例如
synchronized (this){
......
i = i +1 ;
}
7.2. Java中的原子操作有哪些
(1). 除 long 和double 之外的基本類型(int, byte. boolean,short,char,float)的賦值操作
(2). 所有引用reference的賦值操作简识,不管是32位的機器還是64位的機器
(3). Java.concurrent.Atomic. 包中所有類的原子操作*
7.3. long和double的原子性
(1). 問題描述:官方文檔、對于64位的值的寫入,可以分為兩個32位的操作進行寫入七扰、
讀取錯誤奢赂、使用volatile解決
(2). 結(jié)論:在32位上的JVM上。long 和double 的操作不是原子的颈走,但是在64位的JVM是原子的
(3). 實際開發(fā)中:商用Java虛擬機中不會出現(xiàn)
7.4 原子操作+ 原子操作 膳灶!= 原子操作
(1). 簡單地把原子操作組合在一起,并不能保證整體依賴具有原子性
(2). 比如我去ATM機兩次取錢是兩次獨立的原子操作立由,但是期間有可能銀行卡被借給
別人轧钓,也就是被其它線程打斷并被修改。
(3). 全同步的HashMap也不完全安全
8. 單例模式
8.1. 單例模式的作用
為什么需要單例拆吆?
節(jié)省內(nèi)存和計算聋迎,保證結(jié)果正確,方便管理枣耀。
8.2 單例模式的適用場景
- 無狀態(tài)的工具類:比如日志工具類霉晕,不管是在哪里使用,我們需要的只是它幫我們記錄日志信息捞奕,除此之外牺堰,并不需要在它的實例對象上存儲任務(wù)狀態(tài),這時候我們就只需要一個實例對象即可颅围。
- 全局信息類:比如我們在一個類上記錄網(wǎng)站的訪問次數(shù)伟葫,我們不希望有的訪問被記錄在對象A上,有的卻記錄在對象B上院促,這時候我們就讓這個類成為單例筏养。
8.3. 單例模式的八種寫法
(1). 餓漢式(靜態(tài)常量) [可用]
優(yōu)點:這種寫法比較簡單,就是在類裝載的時候就完成實例化常拓。避免了線程同步問題渐溶。
缺點:在類裝載的時候就完成實例化,沒有達到Lazy Loading的效果弄抬。如果從始至終從未使用過這個實例茎辐,則會造成內(nèi)存的浪費。
public class Singleton1 {
private final static Singleton1 INSTANCE = new Singleton1();
private Singleton1() {
}
public static Singleton1 getInstance() {
return INSTANCE;
}
}
(2).餓漢式(靜態(tài)代碼塊)[可用]
這種方式和上面的方式其實類似掂恕,只不過將類實例化的過程放在了靜態(tài)代碼塊中拖陆,也就是類初始化的時候已經(jīng)加載了。
public class Singleton2 {
private final static Singleton2 INSTANCE;
static {
INSTANCE = new Singleton2();
}
private Singleton2() {
}
public static Singleton2 getInstance() {
return INSTANCE;
}
}
(3).懶漢式(線程不安全) [不可用]
這種寫法起到了Lazy Loading的效果懊亡,但是只能在單線程下使用依啰。如果在多線程下,會產(chǎn)生多個實例孔飒。
public class Singleton3 {
private static Singleton3 instance;
private Singleton3() {
}
public static Singleton3 getInstance() {
if (instance == null) {
instance = new Singleton3();
}
return instance;
}
}
(4).懶漢式(線程安全,同步方法)[不推薦]
解決上面第三種實現(xiàn)方式的線程不安全問題桂对,做個線程同步就可以了鸠匀,
缺點:同步效果導(dǎo)致效率低。
public class Singleton4 {
private static Singleton4 instance;
private Singleton4() {
}
public synchronized static Singleton4 getInstance() {
if (instance == null) {
instance = new Singleton4();
}
return instance;
}
}
(5).懶漢式(線程不安全宅此,同步代碼塊)[不推薦]
即便是修改成同步代碼塊父腕,效果也會跟上面一樣導(dǎo)致多個線程會產(chǎn)出多個實例青瀑。
public class Singleton5 {
private static Singleton5 instance;
private Singleton5() {
}
public static Singleton5 getInstance() {
if (instance == null) {
synchronized (Singleton5.class) {
instance = new Singleton5();
}
}
return instance;
}
}
(6).雙重檢查[推薦用]
Double-Check是兩次if (singleton == null)檢查斥难,這樣就可以保證線程安全了。
這樣群扶,實例化代碼只用執(zhí)行一次镀裤,后面再次訪問時,判斷if (singleton == null)馁菜,直接return實例。
優(yōu)點:線程安全峭火;延遲加載;效率較高纺且。
使用 volatile 新建對象的好處:
- 新建對象實際上有3個步驟
- 重排序會帶來NPE
- 防止重排序
public class Singleton6 {
private volatile static Singleton6 instance;
private Singleton6() {
}
public static Singleton6 getInstance() {
if (instance == null) {
synchronized (Singleton6.class) {
if (instance == null) {
instance = new Singleton6();
}
}
}
return instance;
}
}
(7).靜態(tài)內(nèi)部類[推薦用]
靜態(tài)內(nèi)部類方式在Singleton7類被裝載時并不會立即實例化载碌,而是在需要實例化時,調(diào)用getInstance方法嫁艇,才會裝載SingletonInstance類步咪,從而完成Singleton7的實例化。
類的靜態(tài)屬性只會在第一次加載類的時候初始化点晴,所以在這里悯周,JVM幫助我們保證了線程的安全性禽翼,在類進行初始化時,別的線程是無法進入的仇矾。
優(yōu)點:更優(yōu)雅的方式解总、規(guī)范
1.保證懶加載
2.線程安全
3.效率特別高
public class Singleton7 {
private Singleton7() {
}
private static class SingletonInstance {
private static final Singleton7 INSTANCE = new Singleton7();
}
public static Singleton7 getInstance() {
return SingletonInstance.INSTANCE;
}
}
(8).枚舉單例(線程安全)[推薦]
優(yōu)點:
1.線程安全
2.只被裝載一次
public class Singleton8 {
private Singleton8() {
}
private enum Singleton {
INSTANCE;
private final Singleton8 instance;
//構(gòu)建枚舉的函數(shù)的時候已經(jīng)被創(chuàng)建了
Singleton() {
instance = new Singleton8();
}
public Singleton8 getInstance() {
return instance;
}
}
public static Singleton8 getInstance() {
return Singleton.INSTANCE.getInstance();
}
8.4.用那種單例的實現(xiàn)方案最好
- Joshua Bloch 大神在《Effective Java》中明確表達過的觀點:
"使用"枚舉實現(xiàn)單例方法雖然還沒有廣泛采用刻盐,
但是單元素的枚舉類型已經(jīng)成為實現(xiàn)Singleton最佳方法
- Joshua Bloch 大神在《Effective Java》中明確表達過的觀點:
- 寫法簡單
- 線程安全有保障
- 避免反序列化破壞單例
8.5.各種寫法的適用場合
- 最好的方法是利用枚舉劳翰,因為還可以防止反序列化重新創(chuàng)建新的對象
- 非線程同步的方法不能使用
- 如果程序一開始要嘉愛的資源太多,那么就應(yīng)該使用懶加載
- 餓漢式如果是對象的創(chuàng)建需要配置文件就不適用
- 懶加載雖然好乙墙,但是靜態(tài)內(nèi)部類這種方式會引入編程復(fù)雜性
8.6.什么是原子操作听想?Java中有哪些原子操作马胧?生成對象的過程是不是原子操作
- 新建一個空的Person 對象
- 把這個對象的地址指向p
- 執(zhí)行Person的構(gòu)造函數(shù)
9.總結(jié)
大致上就把Java 多線程的volatile與單例模式學(xué)習(xí)了解,這是用看某學(xué)習(xí)視頻總結(jié)而來的個人學(xué)習(xí)文章蛙粘。希望自己也能對Java多線基礎(chǔ)鞏固起來。