多線程下的數據安全 再以後的開發中,我們的項目都是運行在伺服器中,而伺服器已經將線程的定義,線程對象的創建,線程的啟動等,都已經實現完了。我們需要做的就是把編寫的程式放到一個多線程的環境下運行!確保這些數據在運行時都是安全的 一、線程存在安全的三個條件 多線程併發 有共用數據 共用數據有修改的行 ...
多線程下的數據安全
再以後的開發中,我們的項目都是運行在伺服器中,而伺服器已經將線程的定義,線程對象的創建,線程的啟動等,都已經實現完了。我們需要做的就是把編寫的程式放到一個多線程的環境下運行!確保這些數據在運行時都是安全的
一、線程存在安全的三個條件
- 多線程併發
- 有共用數據
- 共用數據有修改的行為
只要滿足上面三個條件,線程就會存在安全問題
二、線程同步機制
怎麼去解決線程安全問題,我們採取線程排隊執行來讓它不能併發來解決!
這種機制被稱為:線程同步機制
線程同步會犧牲一部分效率來保證數據安全,因為數據安全比效率更為重要
三、編程模型
1.同步編程模型
線程t1和線程t2,在t1執行的時候,必須等待t2線程執行結束。或者在t2執行的時候,必須等待t1線程執行結束。兩個線程發生了等待關係,這就是同步編程模型,效率較低,同步就是排隊執行!
2.非同步編程模型
線程t1和線程t2,各自執行,t1不幹涉t2,t2不幹涉t1。兩者互不幹擾,不需要進行等待,這種編程模型叫做:非同步編程模型,效率較高。非同步就是併發
四、線程安全代碼示例
模擬兩個線程同時對一個賬戶進行存取款操作
代碼示例:
/*
銀行賬戶
*/
public class Account {
//賬戶
private String snoid;
//餘額
private double balance;
public Account(String snoid, double balance) {
this.snoid = snoid;
this.balance = balance;
}
public String getSnoid() {
return snoid;
}
public void setSnoid(String snoid) {
this.snoid = snoid;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
public Account() {
}
//取款的構造方法
public void withdraw(Double money){
//取款之前的餘額
double before=this.getBalance();
//取款之後的餘額
double after=before-money;
//更新餘額
this.setBalance(after);
}
}
創建一個線程:線程中run方法,一次取5000塊錢
public class AccountThread extends Thread{
//兩個線程必須共用一個賬戶對象
private Account act;
//通過構造方法傳遞過來賬戶對象
public AccountThread(Account act){
this.act=act;
}
public void run() {
//run方法的執行表示取款操作
double money=5000;
act.withdraw(money);
System.out.println(Thread.currentThread().getName()+"賬戶"+act.getSnoid()+"取款成功,您的當前餘額為"+act.getBalance());
}
}
測試一下:
public class AccountTest {
public static void main(String[] args) {
Account act=new Account("act-1",10000);
//創建連個線程
Thread t1=new AccountThread(act);
Thread t2=new AccountThread(act);
t1.setName("t1");
t2.setName("t2");
t1.start();
t2.start();
}
}
//如果一個線程執行,但是執行過取款操作,沒有執行更新操作
//這時候,t2進來執行,獲取到的餘額還是最初的!所以這種併發的程式有一定的風險
例如:這樣
我們明明調用了兩個線程去執行run方法,應該是一次取5000,第二次應該是0才對。那麼現在出現這種情況!說明數據是不安全的
但是這也是有概率,現在我們放大這個概率,讓餘額在執行更新操作前睡1s,那麼就相當於低於個執行的線程,執行完取款操作之後,等了一下第二個線程。那麼第二個先讀取餘額的時候還是讀取到的10000,現在無論執行多少次,無論是哪個先執行都是上圖中的結果!
五、synchronize同步代碼塊
那麼為瞭解決上述問題,怎麼去實現多個線程執行同步操作呢。只能使用我們上面說的排隊了,這裡就需要使用到synchronize同步代碼塊。
** synchronized (){
//線程同步代碼塊
}**
小括弧中的數據必須是線程共用的數據,才能達到多線程排隊
需要那幾個線程同步就寫哪幾個線程的共用對象
例如,在這,我們t1對象和t2對象的共用對象就是this
在java中,任何一個對象都有一把鎖,其實這把鎖就是標記。100個對象100把鎖,一個對象一把鎖!
代碼的執行原理:
1.假設t1和t2線程併發,開始執行一下代碼的時候,有一個先後順序
2.假設t1先執行,遇到了synchronized,這個時候自動找“後面共用對象”的對象鎖,
找到之後,並占有這把鎖,然後執行同步代碼塊中的程式,在程式執行過程中是一直占有
這把鎖的。指代同步代碼塊改變,這把鎖才會釋放
3.假設t1已經占有這把鎖,此時t2也遇到synchronized關鍵字,也會去占用後面共用對象的這把鎖。
結果這把鎖被t1占有,t2只能在同步代碼塊中等待t1的結束,直到t1把同步代碼塊執行結束,t1會歸還
這把鎖此時t2終於等到這把鎖,然後t2占有這把鎖,進入同步代碼塊執行程式
註意:
共用對象一定選擇好,這個共用對象一定是你需要排隊執行的這些線程對象所共用的
代碼示例:
/*
銀行賬戶
*/
public class Account {
//賬戶
private String snoid;
//餘額
private double balance;
public Account(String snoid, double balance) {
this.snoid = snoid;
this.balance = balance;
}
public String getSnoid() {
return snoid;
}
public void setSnoid(String snoid) {
this.snoid = snoid;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
public Account() {
}
//取款的構造方法
public void withdraw(Double money){
//取款之前的餘額
synchronized (this){
double before=this.getBalance();
//取款之後的餘額
double after=before-money;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//更新餘額
//如果一個線程執行,但是執行過取款操作,沒有執行下麵這樣的更新
//這時候,t2進來執行,獲取到的餘額還是最初的!所以這種併發的程式有一定的風險
this.setBalance(after);
}
}
}
輸出:
t1賬戶act-2取款成功,您的當前餘額為5000.0
t2賬戶act-2取款成功,您的當前餘額為0.0
七、存線上程安全的變數
java中有三大變數!
- 實例變數:在堆中
- 靜態變數:在方法區中
- 局部變數:在棧中
局部變數永遠不會存線上程安全問題,因為局部變數在棧中,永遠不會共用!
實例變數在堆中,堆只有一個,靜態變數在方法區中,方法區也只有一個
堆和方法區都是共用的,所以可能存線上程安全問題!
局部變數以及常量都不會存線上程安全問題,成員變數可能會存線上程安全問題!
如果使用局部變數:
建議使用StringBuilder,因為局部變數不存線上程安全問題,選擇StringBuilder效率比StringBuffer效率更高!
ArrayList是非線程安全的
Vector是線程安全的
HashMap HashSet是非線程安全的
Hashtable是線程安全的
本文來自博客園,作者:星餘明,轉載請註明原文鏈接:https://www.cnblogs.com/lingstar/p/16547525.html