共享對(duì)象
使用Java編寫線程安全的程序關(guān)鍵在于正確的使用共享對(duì)象,以及安全的對(duì)其進(jìn)行訪問管理。Java的內(nèi)置鎖可以保障線程安全,對(duì)于其他的應(yīng)用來說并發(fā)的安全性是使用內(nèi)置鎖保障了線程變量使用的邊界。談到線程的邊界問題,隨之而來的是Java內(nèi)存模型另外的一個(gè)重要的含義,可見性。Java對(duì)可見性提供的原生支持是volatile關(guān)鍵字。
Atomic
作用
對(duì)于原子操作類,Java的concurrent并發(fā)包中主要為我們提供了這么幾個(gè)常用的:AtomicInteger、AtomicLong、AtomicBoolean、AtomicReference<T>。
對(duì)于原子操作類,最大的特點(diǎn)是在多線程并發(fā)操作同一個(gè)資源的情況下,使用Lock-Free算法來替代鎖,這樣開銷小、速度快,對(duì)于原子操作類是采用原子操作指令實(shí)現(xiàn)的,從而可以保證操作的原子性。
通常情況下,在Java里面,++i或者--i不是線程安全的,這里面有三個(gè)獨(dú)立的操作:獲得變量當(dāng)前值,為該值+1/-1,然后寫回新的值。在沒有額外資源可以利用的情況下,只能使用加鎖才能保證讀-改-寫這三個(gè)操作是“原子性”的。
Java 5新增了AtomicInteger類,該類包含方法getAndIncrement()以及getAndDecrement(),這兩個(gè)方法實(shí)現(xiàn)了原子加以及原子減操作,但是比較不同的是這兩個(gè)操作沒有使用任何加鎖機(jī)制,屬于無鎖操作。
它會(huì)在這步操作都完成情況下才允許其它線程再對(duì)它進(jìn)行操作,而這個(gè)實(shí)現(xiàn)則是通過Lock-Free+原子操作指令來確定的
AtomicInteger類中:
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
public final int get() {
return value;
}
private volatile int value;
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
可以看到是一個(gè)cas原子操作。
unsafe是java用來在CPU級(jí)別的操作CAS指令的類,對(duì)于程序員來說,此類是不可用。
由于是cpu級(jí)別的指令,其開銷比需要操作系統(tǒng)參與的鎖的開銷小。
對(duì)于多個(gè)線程進(jìn)入時(shí),會(huì)先比較現(xiàn)在的value 是否與expect相等,如果不相等,則進(jìn)入下一個(gè)循環(huán)。如果相等,則會(huì)更新成update值。
之后再進(jìn)入的線程則會(huì)死循環(huán)。這樣就保證了操作的原子性。
這樣一個(gè)方法中 即包含了原子性,又包含了可見性
而關(guān)于Lock-Free算法,則是一種新的策略替代鎖來保證資源在并發(fā)時(shí)的完整性的,Lock-Free的實(shí)現(xiàn)有三步:
- 循環(huán)(for(;;)、while)
- CAS(CompareAndSet)
- 回退(return、break)
用法
比如在多個(gè)線程操作一個(gè)count變量的情況下,則可以把count定義為AtomicInteger,如下:
public class Counter {
private AtomicInteger count = new AtomicInteger();
public int getCount() {
return count.get();
}
public void increment() {
count.incrementAndGet();
}
在每個(gè)線程中通過increment()來對(duì)count進(jìn)行計(jì)數(shù)增加的操作,或者其它一些操作。這樣每個(gè)線程訪問到的將是安全、完整的count。
內(nèi)部實(shí)現(xiàn)
采用Lock-Free算法替代鎖+原子操作指令實(shí)現(xiàn)并發(fā)情況下資源的安全、完整、一致性
ABA問題(AtomicStampedReference的使用)
public class ABA {
// 普通的原子類,存在ABA問題
AtomicInteger a1 = new AtomicInteger(10);
// 帶有時(shí)間戳的原子類,不存在ABA問題,第二個(gè)參數(shù)就是默認(rèn)時(shí)間戳,這里指定為0
AtomicStampedReference<Integer> a2 = new AtomicStampedReference<Integer>(10, 0);
public static void main(String[] args) {
ABA a = new ABA();
a.test();
}
public void test() {
new Thread1().start();
new Thread2().start();
new Thread3().start();
new Thread4().start();
}
class Thread1 extends Thread {
@Override
public void run() {
a1.compareAndSet(10, 11);
a1.compareAndSet(11, 10);
}
}
class Thread2 extends Thread {
@Override
public void run() {
try {
Thread.sleep(200); // 睡0.2秒,給線程1時(shí)間做ABA操作
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("AtomicInteger原子操作:" + a1.compareAndSet(10, 11));
}
}
class Thread3 extends Thread {
@Override
public void run() {
try {
Thread.sleep(500); // 睡0.5秒,保證線程4先執(zhí)行
} catch (InterruptedException e) {
e.printStackTrace();
}
int stamp = a2.getStamp();
a2.compareAndSet(10, 11, stamp, stamp + 1);
stamp = a2.getStamp();
a2.compareAndSet(11, 10, stamp, stamp + 1);
}
}
class Thread4 extends Thread {
@Override
public void run() {
int stamp = a2.getStamp();
try {
Thread.sleep(1000); // 睡一秒,給線程3時(shí)間做ABA操作
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("AtomicStampedReference原子操作:" + a2.compareAndSet(10, 11, stamp, stamp + 1));
}
}
}
Volatile
作用
Volatile可以看做是一個(gè)輕量級(jí)的synchronized,它可以在多線程并發(fā)的情況下保證變量的“可見性”,
什么是可見性?
就是在一個(gè)線程的工作內(nèi)存中修改了該變量的值,該變量的值立即能回顯到主內(nèi)存中,從而保證所有的線程看到這個(gè)變量的值是一致的,其二 volatile 禁止了指令重排,所以在處理同步問題上它大顯作用,而且它的開銷比synchronized小、使用成本更低。
雖然 volatile 變量具有可見性和禁止指令重排序,但是并不能說 volatile 變量能確保并發(fā)安全。
舉個(gè)栗子:在寫單例模式中,除了用靜態(tài)內(nèi)部類外,還有一種寫法也非常受歡迎,就是Volatile+DCL:
public class Singleton {
private static volatile Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
這樣單例不管在哪個(gè)線程中創(chuàng)建的,所有線程都是共享這個(gè)單例的。
雖說這個(gè)Volatile關(guān)鍵字可以解決多線程環(huán)境下的同步問題,不過這也是相對(duì)的,因?yàn)樗痪哂胁僮鞯脑有裕簿褪撬贿m合在對(duì)該變量的寫操作依賴于變量本身自己。舉個(gè)最簡單的栗子:在進(jìn)行計(jì)數(shù)操作時(shí)count++,實(shí)際是count=count+1;,count最終的值依賴于它本身的值。所以使用volatile修飾的變量在進(jìn)行這么一系列的操作的時(shí)候,就有并發(fā)的問題 .
volatile只能確保操作的是同一塊內(nèi)存,并不能保證操作的原子性。所以volatile一般用于聲明簡單類型變量,使得這些變量具有原子性,即一些簡單的賦值與返回操作將被確保不中斷。但是當(dāng)該變量的值由自身的上一個(gè)決定時(shí),volatile的作用就將失效,這是由volatile關(guān)鍵字的性質(zhì)所決定的。
所以在volatile時(shí)一定要謹(jǐn)慎,千萬不要以為用volatile修飾后該變量的所有操作都是原子操作,不再需要synchronized關(guān)鍵字了。
用法
因?yàn)関olatile不具有操作的原子性,所以如果用volatile修飾的變量在進(jìn)行依賴于它自身的操作時(shí),就有并發(fā)問題,如:count,像下面這樣寫在并發(fā)環(huán)境中是達(dá)不到任何效果的:
public class Counter {
private volatile int count;
public int getCount(){
return count;
}
public void increment(){
count++;
}
}
而要想count能在并發(fā)環(huán)境中保持?jǐn)?shù)據(jù)的一致性,則可以在increment()中加synchronized同步鎖修飾,改進(jìn)后的為:
public class Counter {
private volatile/無 int count;
public int getCount(){
return count;
}
public synchronized void increment(){
count++;
}
}
內(nèi)部實(shí)現(xiàn)
匯編指令實(shí)現(xiàn)
Synchronized
作用
synchronized關(guān)鍵字是Java利用鎖的機(jī)制自動(dòng)實(shí)現(xiàn)的,一般有同步方法和同步代碼塊兩種使用方式。Java中所有的對(duì)象都自動(dòng)含有單一的鎖(也稱為監(jiān)視器),當(dāng)在對(duì)象上調(diào)用其任意的synchronized方法時(shí),此對(duì)象被加鎖(一個(gè)任務(wù)可以多次獲得對(duì)象的鎖,計(jì)數(shù)會(huì)遞增),同時(shí)在線程從該方法返回之前,該對(duì)象內(nèi)其他所有要調(diào)用類中被標(biāo)記為synchronized的方法的線程都會(huì)被阻塞。當(dāng)然針對(duì)每個(gè)類也有一個(gè)鎖(作為類的Class對(duì)象的一部分),所以你懂的.。
正因?yàn)樗谶@種阻塞的策略,所以它的性能不太好,但是由于操作上的優(yōu)勢,只需要簡單的聲明一下即可,而且被它聲明的代碼塊也是具有操作的原子性。
最后需要注意的是synchronized是同步機(jī)制中最安全的一種方式,其他的任何方式都是有風(fēng)險(xiǎn)的,當(dāng)然付出的代價(jià)也是最大的。
用法
public synchronized void increment(){
count++;
}
public void increment(){
synchronized (Counte.class){
count++;
}
}
內(nèi)部實(shí)現(xiàn)
ThreadLocal
作用
而ThreadLocal的設(shè)計(jì),并不是解決資源共享的問題,而是用來提供線程內(nèi)的局部變量,這樣每個(gè)線程都自己管理自己的局部變量,別的線程操作的數(shù)據(jù)不會(huì)對(duì)我產(chǎn)生影響,互不影響,所以不存在解決資源共享這么一說,如果是解決資源共享,那么其它線程操作的結(jié)果必然我需要獲取到,而ThreadLocal則是自己管理自己的,相當(dāng)于封裝在Thread內(nèi)部了,供線程自己管理,這樣做其實(shí)就是以空間換時(shí)間的方式(與synchronized相反),以耗費(fèi)內(nèi)存為代價(jià),單大大減少了線程同步(如synchronized)所帶來性能消耗以及減少了線程并發(fā)控制的復(fù)雜度。
用法
ThreadLocal實(shí)例通常來說都是private static類型的,用于關(guān)聯(lián)線程和線程的上下文
一般使用ThreadLocal,官方建議我們定義為private static ,至于為什么要定義成靜態(tài)的,這和內(nèi)存泄露有關(guān),后面再討論。
它有三個(gè)暴露的方法,set、get、remove。
public class TestThreadLocal {
private static final ThreadLocal<Integer> value = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0;
}
};
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(new MyThread(i)).start();
}
}
static class MyThread implements Runnable {
private int index;
public MyThread(int index) {
this.index = index;
}
public void run() {
System.out.println("線程" + index + "的初始value:" + value.get());
for (int i = 0; i < 10; i++) {
value.set(value.get() + i);
}
System.out.println("線程" + index + "的累加value:" + value.get());
}
}
}
運(yùn)行結(jié)果如下,這些ThreadLocal變量屬于線程內(nèi)部管理的,互不影響:
線程0的初始value:0
線程3的初始value:0
線程2的初始value:0
線程2的累加value:45
線程1的初始value:0
線程3的累加value:45
線程0的累加value:45
線程1的累加value:45
線程4的初始value:0
線程4的累加value:45
對(duì)于get方法,在ThreadLocal沒有set值得情況下,默認(rèn)返回null,所有如果要有一個(gè)初始值我們可以重寫initialValue()方法,在沒有set值得情況下調(diào)用get則返回初始值。
內(nèi)部實(shí)現(xiàn)
ThreadLocal內(nèi)部有一個(gè)靜態(tài)類ThreadLocalMap,使用到ThreadLocal的線程會(huì)與ThreadLocalMap綁定,維護(hù)著這個(gè)Map對(duì)象,而這個(gè)ThreadLocalMap的作用是映射當(dāng)前ThreadLocal對(duì)應(yīng)的值,它key為當(dāng)前ThreadLocal的弱引用:WeakReference
內(nèi)存泄露問題
對(duì)于ThreadLocal,一直涉及到內(nèi)存的泄露問題,即當(dāng)該線程不需要再操作某個(gè)ThreadLocal內(nèi)的值時(shí),應(yīng)該手動(dòng)的remove掉,為什么呢?我們來看看ThreadLocal與Thread的聯(lián)系圖:
此圖來自網(wǎng)絡(luò):
[圖片上傳失敗...(image-b3d685-1606987896368)]
其中虛線表示弱引用,從該圖可以看出,一個(gè)Thread維持著一個(gè)ThreadLocalMap對(duì)象,而該Map對(duì)象的key又由提供該value的ThreadLocal對(duì)象弱引用提供,所以這就有這種情況:
如果ThreadLocal不設(shè)為static的,由于Thread的生命周期不可預(yù)知,這就導(dǎo)致了當(dāng)系統(tǒng)gc時(shí)將會(huì)回收它,而ThreadLocal對(duì)象被回收了,此時(shí)它對(duì)應(yīng)key必定為null,這就導(dǎo)致了該key對(duì)應(yīng)得value拿不出來了,而value之前被Thread所引用,所以就存在key為null、value存在強(qiáng)引用導(dǎo)致這個(gè)Entry回收不了,從而導(dǎo)致內(nèi)存泄露。
所以避免內(nèi)存泄露的方法,是對(duì)于ThreadLocal要設(shè)為static靜態(tài)的,
這樣的話ThreadLocal的生命周期就更長,由于一直存在ThreadLocal的強(qiáng)引用,所以ThreadLocal也就不會(huì)被回收,也就能保證任何時(shí)候都能根據(jù)ThreadLocal的弱引用訪問到Entry的value值,然后remove它,防止內(nèi)存泄露。除了這個(gè),還必須在線程不使用它的值是手動(dòng)remove掉該ThreadLocal的值,這樣Entry就能夠在系統(tǒng)gc的時(shí)候正?;厥?,而關(guān)于ThreadLocalMap的回收,會(huì)在當(dāng)前Thread銷毀之后進(jìn)行回收。
但需要注意的是,雖然ThreadLocal和Synchonized都用于解決多線程并發(fā)訪問,ThreadLocal與synchronized還是有本質(zhì)的區(qū)別。synchronized是利用鎖的機(jī)制,使變量或代碼塊在某一時(shí)該只能被一個(gè)線程訪問。而ThreadLocal為每一個(gè)線程都提供了變量的副本,使得每個(gè)線程在某一時(shí)間訪問到的并不是同一個(gè)對(duì)象,這樣就隔離了多個(gè)線程對(duì)數(shù)據(jù)的數(shù)據(jù)共享。而Synchronized卻正好相反,它用于在多個(gè)線程間通信時(shí)能夠獲得數(shù)據(jù)共享。即Synchronized用于線程間的數(shù)據(jù)共享,而ThreadLocal則用于線程間的數(shù)據(jù)隔離。所以ThreadLocal并不能代替synchronized,Synchronized的功能范圍更廣(同步機(jī)制)。
- 補(bǔ)充
InheritableThreadLocal
ThreadLocal類固然很好,但是子線程并不能取到父線程的ThreadLocal類的變量,InheritableThreadLocal類就是解決這個(gè)問題的。
/**
*TODO 驗(yàn)證線程變量間的隔離性
*/
public class Test3 {
public static void main(String[] args) {
try {
for (int i = 0; i < 10; i++) {
System.out.println(" 在Main線程中取值=" + Tools.tl.get());
Thread.sleep(100);
}
Thread.sleep(5000);
ThreadA a = new ThreadA();
a.start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/*static public class Tools {
public static ThreadLocalExt tl = new ThreadLocalExt();
}
static public class ThreadLocalExt extends ThreadLocal {
@Override
protected Object initialValue() {
return new Date().getTime();
}
}*/
static public class Tools {
public static InheritableThreadLocalExt tl = new InheritableThreadLocalExt();
}
static public class InheritableThreadLocalExt extends InheritableThreadLocal {
@Override
protected Object initialValue() {
return new Date().getTime();
}
@Override
protected Object childValue(Object parentValue) {
return parentValue + " 我在子線程加的~!";
}
}
static public class ThreadA extends Thread {
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
System.out.println("在ThreadA線程中取值=" + Tools.tl.get());
Thread.sleep(100);
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
在使用InheritableThreadLocal類需要注意的一點(diǎn)是:如果子線程在取得值的同時(shí),主線程將InheritableThreadLocal中的值進(jìn)行更改,那么子線程取到的還是舊值。
總結(jié)
關(guān)于Volatile關(guān)鍵字具有可見性,但不具有操作的原子性,而synchronized比volatile對(duì)資源的消耗稍微大點(diǎn),但可以保證變量操作的原子性,保證變量的一致性,最佳實(shí)踐則是二者結(jié)合一起使用。
對(duì)于synchronized的出現(xiàn),是解決多線程資源共享的問題,同步機(jī)制采用了“以時(shí)間換空間”的方式:訪問串行化,對(duì)象共享化。同步機(jī)制是提供一份變量,讓所有線程都可以訪問。
對(duì)于Atomic的出現(xiàn),是通過原子操作指令+Lock-Free完成,從而實(shí)現(xiàn)非阻塞式的并發(fā)問題。
對(duì)于Volatile,為多線程資源共享問題解決了部分需求,在非依賴自身的操作的情況下,對(duì)變量的改變將對(duì)任何線程可見。
ThreadLocal的作用是提供線程內(nèi)的局部變量,這種變量在線程的生命周期內(nèi)起作用,減少同一個(gè)線程內(nèi)多個(gè)函數(shù)或者組件之間一些公共變量的傳遞的復(fù)雜度。