線程安全
線程安全的概念不容易定義,在《Java 併發編程實踐》中,作者做出瞭如此定義:多個線程訪問一個類對象時,如果不用考慮這些線程在運行時環境下的調度和交替執行,並且不需要額外的同步及在調用方法代碼不必作其他的協調,這個類的行爲仍然是正確的,那麼這個類是線程安全的。
也就是說一堆線程去操作一個方法去控制同一個資源,由於是交替執行的,可能會出現一個數據一個線程正在運算還沒來得急把數據寫進去,結果被另外一個線程把這個數據的髒數據讀取出去了。
這樣說,可能有些朋友沒有看明白,那麼我們先來看一個示例,來演示一下什麼是現成不安全的情況。
publicclass SafeThreadTest {
V v = new V();
publicstaticvoid main(String[] args) {
SafeThreadTest test = new SafeThreadTest();
test.test();
}
/**
* 開兩個線程,分別調用V對象的打印字符串的方法
*/
publicvoid test(){
new Thread(new Runnable() {
@Override
publicvoid run() {
while(true){
v.printString("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
}
}
}).start();
new Thread(new Runnable() {
@Override
publicvoid run() {
while(true){
v.printString("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB");
}
}
}).start();
}
/**
* 這個類負責打印字符串
* @author Administrator
*/
class V {
//創建一個鎖對象
Lock lock = new ReentrantLock();
/**
* 爲了能使方法運行速度減慢,我們一個字符一個字符的打印
* @param s
*/
publicvoid printString(String s){
//加鎖,只允許一個線程訪問
lock.lock();
try {
for(int i = 0;i<s.length();i++){
System.out.print(s.charAt(i));
}
System.out.println();
}finally{
//解鎖,值得注意的是,這裏鎖的釋放放到了finally代碼塊中,保證解鎖工作一定會執行
lock.unlock();
}
}
}
}
使用這樣的方式,與使用synchronized的功能一樣,只不過這樣使代碼看起來更加面向對象一些,怎麼加鎖,怎麼解鎖一目瞭然。
另外,ReentrantLock其實比synchronized增加了一些功能,主要有:
等待可中斷
這是指的當前持有鎖的線程如果長期不釋放鎖,正在等待的線程可以放棄等待,處理其他事情。
公平鎖
這個是說多個線程在等待同一個鎖的時候,必須按照申請鎖的時間順序依次獲得鎖。synchronized中的鎖是不公平鎖,鎖被釋放的時候任何一個等待鎖的線程都有機會獲得鎖,ReentrantLock默認也是不公平鎖,可以使用構造函數使得其爲公平鎖。如果爲true代表公平鎖Lock lock = new ReentrantLock(true);
綁定條件
這個就是的條件鎖。
在性能上,在jdk1.6之前的版本,使用ReentrantLock的性能要好於使用synchronized。而在jdk1.6開始,兩者性能均差不多。
條件鎖
使用ReentrantLock類對象可以實現條件鎖,這個類的對象有一個newCondition()方法,可以返回Condition對象,放在類裏面作爲成員變量,在被鎖住的代碼塊裏面調用裏面的方法,該對象的await方法代表把該線程休眠,調用該對象的signl方法代表把這個對象休眠的那個線程叫醒。
條件鎖可以實現三個線程的輪循,比如三個線程分別執行while(true)死循環來執行三個不同的方法,要求是實現三個方法的輪循執行,A方法完了是B方法,然後是C方法,然後再A方法,那就可以在ABC的方法類裏面定義一個標誌int 型變量代表該誰執行了,ReentrantLock,使用newCondition()方法生成三個Condition(條件)c1,c2,c3,在方法A裏面如果標誌不是1則使用while阻塞掉,循環中使用c1.await()讓A睡眠,如果A被c1.signl()叫醒了,則執行裏面需要執行的方法體,並且把標誌改爲2表示該B方法執行了,調用c2.signl()方法叫醒B方法,如此這般三個方法有序的輪循。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Lunxun {
V v = new V();
public static void main(String[] args) {
Lunxun lx = new Lunxun();
lx.go();
}
private void go() {
new Thread(new Runnable() {
@Override
public void run() {
while(true){
v.m1();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while(true){
v.m2();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while(true){
v.m3();
}
}
}).start();
}
class V {
int token = 1;
Lock lock = new ReentrantLock();
Condition c1 = lock.newCondition();
Condition c2 = lock.newCondition();
Condition c3 = lock.newCondition();
void m1(){
lock.lock();
while(token!=1){
try {
c1.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("AAAAAAAAAAAAAAAAAAAAA");
token = 2;
c2.signal();
lock.unlock();
}
void m2(){
lock.lock();
while(token!=2){
try {
c2.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("BBBBBBBBBBBBBBBBBBBBB");
token = 3;
c3.signal();
lock.unlock();
}
void m3(){
lock.lock();
while(token!=3){
try {
c3.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("CCCCCCCCCCCCCCCCCCCCCC");
token = 1;
c1.signal();
lock.unlock();
}
}
}
讀寫鎖
大量線程併發訪問並修改和讀取一個資源的時候,爲了兩全安全和性能,可以使用讀寫鎖,意思是在線程進行寫操作的時候,其他線程都不能進行讀和寫操作,而但有線程進行讀操作的時候,其他線程都可以對該資源進行讀操作,但不能進行寫操作。
看下面一個小程序,用對象v來模擬一個數據對象,裏面有讀和寫兩個方法,開5個讀線程5個寫線程分別while(true)不斷的讀寫數據,如何防止10個線程出現寫與寫,讀和寫的衝突呢?
實例如下:
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class TestReadWriteLock {
Random r = new Random();
V v = new V();
public static void main(String[] args) {
final TestReadWriteLock trwl = new TestReadWriteLock();
for (int i = 0; i < 3; i++) {
new Thread(new Runnable() {
@Override
public void run() {
while(true){
trwl.v.getdata(trwl.r.nextInt(100));
}
}
},"讀線程"+ i).start();
}
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
@Override
public void run() {
while(true){
trwl.v.setdata(trwl.r.nextInt(100));
}
}
},"寫線程"+ i).start();
}
}
class V {
List<Integer> list = new ArrayList<Integer>();
ReadWriteLock rw = new ReentrantReadWriteLock() ;
public V(){
for (int i = 0; i < 100; i++) {
list.add(i);
}
}
void setdata(int i){
//rw.writeLock().lock();
//try {
System.out.println(Thread.currentThread().getName() + "正在寫數據中》》》》》》》》》》》》》");
try {
Thread.sleep(r.nextInt(1000));
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
list.add(i);
System.out.println(Thread.currentThread().getName() + "寫數據完畢!");
//} catch (Exception e) {
//e.printStackTrace();
//}finally{
//rw.writeLock().unlock();
//}
}
@SuppressWarnings("finally")
int getdata(int i){
//rw.readLock().lock();
int result;
//try {
System.out.println(Thread.currentThread().getName() + "正在讀取數據《《《《《《《《《《《《《《");
try {
Thread.sleep(r.nextInt(1000));
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
result = list.get(i);
System.out.println(Thread.currentThread().getName() + "讀取完畢!");
return result;
//} catch (Exception e) {
//// TODO Auto-generated catch block
//e.printStackTrace();
//}finally{
//rw.readLock().unlock();
//return 0;
//}
}
}
}
如果不使用讀寫鎖,而使用synchronized我們稱之爲重量鎖或者ReentranLock我們稱之爲輕量鎖,雖然保證安全,但無法保證讀和讀不互斥。而不使用鎖,將上面代碼裏的讀寫鎖註釋起來,則會出現在數據被寫入的過程中有線程插進來讀寫數據。而讀寫鎖使得安全和性能得到兩全,因爲畢竟讀比寫要頻繁的多,讀和讀不互斥可以很大程度上提高性能。
以下是不加鎖的運行結果:
讀線程1正在讀取數據《《《《《《《《《《《《《《
讀線程2正在讀取數據《《《《《《《《《《《《《《
讀線程0正在讀取數據《《《《《《《《《《《《《《
寫線程0正在寫數據中》》》》》》》》》》》》》
寫線程1正在寫數據中》》》》》》》》》》》》》
寫線程2正在寫數據中》》》》》》》》》》》》》
寫線程3正在寫數據中》》》》》》》》》》》》》
寫線程4正在寫數據中》》》》》》》》》》》》
我們會發現在0號寫線程在寫數據的時候,1號寫線程也同時往裏面寫數據,這是我們最不想看到的情況。
而加上讀寫鎖之後再看:
讀線程1正在讀取數據《《《《《《《《《《《《《《
讀線程0正在讀取數據《《《《《《《《《《《《《《
讀線程2正在讀取數據《《《《《《《《《《《《《《
讀線程1讀取完畢!
讀線程2讀取完畢!
讀線程0讀取完畢!
寫線程2正在寫數據中》》》》》》》》》》》》》
寫線程2寫數據完畢!
寫線程2正在寫數據中》》》》》》》》》》》》》
寫線程2寫數據完畢!
寫線程2正在寫數據中》》》》》》》》》》》》》
寫線程2寫數據完畢!
寫線程2正在寫數據中》》》》》》》》》》》》》
寫線程2寫數據完畢!
寫線程0正在寫數據中》》》》》》》》》》》》》
寫線程0寫數據完畢!
寫線程4正在寫數據中》》》》》》》》》》》》》
寫線程4寫數據完畢!
寫線程3正在寫數據中》》》》》》》》》》》》》
寫線程3寫數據完畢!
寫線程3正在寫數據中》》》》》》》》》》》》》
讀線程1 2 0可以同時讀數據,此時沒有寫線程在寫數據,而當2號寫線程在寫數據的時候其他所有線程都在老老實實等着2號寫線程把數據寫完纔開始進行讀寫操作。
再補充一點讀寫鎖的重要功能,重入和降級(照抄API)
·重入
此鎖允許 reader 和 writer 按照 ReentrantLock 的樣式重新獲取讀取鎖或寫入鎖。在寫入線程保持的所有寫入鎖都已經釋放後,才允許重入 reader 使用它們。
此外,writer 可以獲取讀取鎖,但反過來則不成立。在其他應用程序中,當在調用或回調那些在讀取鎖狀態下執行讀取操作的方法期間保持寫入鎖時,重入很有用。如果 reader 試圖獲取寫入鎖,那麼將永遠不會獲得成功。
·鎖降級
重入還允許從寫入鎖降級爲讀取鎖,其實現方式是:先獲取寫入鎖,然後獲取讀取鎖,最後釋放寫入鎖。但是,從讀取鎖升級到寫入鎖是不可能的。
·鎖獲取的中斷
讀取鎖和寫入鎖都支持鎖獲取期間的中斷。
推薦java API文檔的例子,例子使用的僞代碼如下:
class CachedData {
Object data;
volatile boolean cacheValid;
ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();①
if (!cacheValid) {
// Must release read lock before acquiring write lock
rwl.readLock().unlock();②
rwl.writeLock().lock();③
// Recheck state because another thread might have acquired
// write lock and changed state before we did.
if (!cacheValid) {
data = ...④
cacheValid = true;
}
// Downgrade by acquiring read lock before releasing write lock
rwl.readLock().lock();⑤
rwl.writeLock().unlock(); // Unlock write, still hold read⑥
}
use(data);
rwl.readLock().unlock();⑦
}
}
這是一個典型的緩存應用問題,可以這樣理解:假設A線程和B線程同時第一次進入processCachedData方法,均對這個方法加了讀鎖。這個時候cacheValid值爲false,如果這個時候沒有②和③處的代碼,也就是說沒有加寫鎖,會導致A線程將結果放到緩存中,B線程也會將結果放到緩存中,導致衝突。而②和③處的代碼正是先釋放讀鎖,然後加上寫鎖,使得另外的線程等待。然後在代碼④處爲緩存賦值。在代碼⑤處先加上讀鎖,然後在代碼⑥處釋放寫鎖,然後use(data)是模擬使用數據,最後在代碼⑦處釋放寫鎖。這樣保證了緩存的有效性。另外筆者指出一點:這裏涉及到一個概念叫做鎖降級。鎖降級是說允許從寫入鎖降級爲讀取鎖,其實現方式是:先獲取寫入鎖,然後獲取讀取鎖,最後釋放寫入鎖。但是,不能從讀鎖升級到寫鎖。爲的就是防止你直接放棄寫入鎖的話該線程就沒有鎖了,其他線程尤其是讀線程一旦進入就會出現安全問題,因爲你的整個讀操作還沒有完成,不允許寫。