一、線程安全問題(銀行取錢)
問題描述:
當兩個人同時對一個賬戶進行操作取錢的時候,可能會出現線程安全問題
//定義一個用戶類
public class Account {
// 銀行賬戶
private String accountNo;
//餘額
private int balance;
public Account(String accountNo, int balance) {
this.accountNo = accountNo;
this.balance = balance;
}
// get() set() 方法
}
// 取錢的類
public class DrawThread extends Thread {
// 取錢的賬戶
private Account account;
// 取錢的金額
private int monny;
DrawThread(String name,Account account,int monny){
super(name);
this.account = account;
this.monny = monny;
}
@Override
public void run() {
super.run();
if (account.getBalance() >= monny){
Log.e("testthread",getName()+"取錢成功"+monny);
// 強制線程調度切換,這樣每次兩個用戶都能取到錢了
Thread.sleep(1);
account.setBalance(account.getBalance()-monny);
}else {
Log.e("testthread","餘額不足~~");
}
}
}
//取錢
mBntFun5.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Account account = new Account("admin",1000);
DrawThread thread1 = new DrawThread("用戶1",account,800);
thread1.start();
DrawThread thread2 = new DrawThread("用戶2",account,800);
thread2.start();
}
});
上面代碼很有可能會執行成功,如上圖1,或者如圖2;
要每次都出現圖1的異常情況,只需要將run()中的 Thread.sleep(1); 打開即可
二、同步代碼塊
圖一是因爲run()方法的方法體不具有同步安全性,可以用同步監視器解決這個問題,通用方法就是同步代碼塊,語法格式如下:
// obj 就是同步監視器
synchronized(obj){
...
// 此處就是同步代碼塊
}
說明
- 上面代碼的說明:就是在執行同步代碼塊之前,必須要對同步監視器進行鎖定
- 任何時刻只能有一個線程可以獲得同步監視器的鎖定,當同步代碼塊執行完之後,就會釋放同步監視器的鎖定
- 同步監視器 一般都是 可能被併發訪問的共享資源
- 一般邏輯如下:
加鎖–>修改–>釋放鎖
上面的代碼進行優化,如下:
@Override
public void run() {
super.run();
// 符合加鎖-->修改-->釋放鎖的邏輯
synchronized(account){
if (account.getBalance() >= monny){
Log.e("testthread",getName()+"取錢成功"+monny);
try {
// 強制線程調度切換,這樣每次兩個用戶都能取到錢了
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.setBalance(account.getBalance()-monny);
Log.e("testthread","餘額:"+account.getBalance());
}else {
Log.e("testthread","餘額不足~~");
}
}
}
三、同步方法
-
用synchronized關鍵字修飾的方法就是同步方法,無需顯示指定同步監視器,它的同步監視器就是this,也就是該對象本身
-
線程安全的類具有如下特點:
(1)該類的對象可以被多個線程同時訪問
(2)每個線程調用該類的方法後返回的都是正確結果
(3)每個線程調用該對象的方法後,該對象的狀態仍然保持合理狀態 -
不要對所有的方法都進行同步,只對共享資源進行同步
-
如果可變類有兩種運行環境:單線程和多線程,則要爲它提供兩種版本:線程安全版本和線程不安全版本;
- 單線程中使用線程不安全版本保證性能
- 多線程中使用線程安全版本
public class Account {
// 銀行賬戶
private String accountNo;
//餘額
private int balance;
public Account(String accountNo, int balance) {
this.accountNo = accountNo;
this.balance = balance;
}
public synchronized void draw(int monny){
if (balance >= monny){
Log.e("testthread",Thread.currentThread().getName()+"取錢成功:"+monny);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
setBalance(balance - monny);
}else {
Log.e("testthread",Thread.currentThread().getName()+"取錢失敗");
}
Log.e("testthread","餘額:"+getBalance());
}
}
@Override
public void run() {
super.run();
account.draw(800);
}
說明
- synchronized 要寫在返回值的前面
- 因爲draw()用synchronized修飾了,同步方法的同步監視器總是 this,而this指的是調用這個方法的對象。在上面的代碼中,調用draw()的是draw,因此多個線程併發修改account的時候,要先對account對象進行加鎖。
四、釋放同步監視器的鎖定
- 以下幾種情況會釋放同步監視器:
- 當同步代碼塊或同步方法執行完了,會釋放;
- 當同步代碼塊或同步方法中遇到了break、return 進行終止,會釋放
- 當同步代碼塊或同步方法中有未處理的Error或Exception,導致了異常退出,會釋放
- 當執行了同步監視器的ewait()方法,當前線程會暫停,並釋放
- 以下幾種情況不會釋放:
- 在同步代碼塊或同步方法執行的時候,程序調用了Thread.sleep()或Thread.yield()來暫停當前線程,則不會釋放
- 在同步代碼塊或同步方法執行的時候,程序調用了suspend使線程掛起了,則不會釋放。應該儘量避免使用suspend和resume來控制線程
五、同步鎖(Lock)
- Lock 是sychronized的升級版,有跟廣泛的鎖定操作
- Lock是控制多個線程對共享資源進行訪問的工具,提供了對共享資源的獨佔訪問
- java提供了兩個根接口:
- Lock---->實現類:ReentranLock(可重入鎖)
- ReadWriteLock—>實現類:ReentrantReadWiteLock
可重用性:
一個線程可以對已經加鎖的ReentranLock再次加鎖,線程每次調用lock()加鎖後,都必須顯示調用unlock()釋放鎖
使用格式:
class A{
ReentrantLock lock = new ReentrantLock();
void fun(){
//加鎖
lock.lock();
try {
//需要保證線程安全的代碼
// .....
}
finally {
// 釋放鎖
lock.unlock();
}
}
}
以上取錢的代碼進行優化:
public class Account {
// 銀行賬戶
private String accountNo;
//餘額
private int balance;
ReentrantLock lock = new ReentrantLock();
public void drawmonny(int monny){
// 加鎖
lock.lock();
try {
if (balance >= monny){
Log.e("testthread",Thread.currentThread().getName()+"取錢成功:"+monny);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
setBalance(balance - monny);
}else {
Log.e("testthread",Thread.currentThread().getName()+"取錢失敗");
}
Log.e("testthread","餘額:"+getBalance());
}
finally {
// 釋放鎖
lock.unlock();
}
}
}
六、死鎖(互相等待釋放同步監視器)
當兩個線程相互等待對方釋放同步監視器的時候就會發生死鎖,死鎖發生的時候既不會發生異常,也不會給任何提示,所以要儘量避免死鎖。當系統中有多個同步監視器的時候就很容易發生死鎖。
public class A{
public synchronized void funA(B b){
Log.e("testthread",Thread.currentThread().getName()+"進入A的fun方法");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.e("testthread",Thread.currentThread().getName()+"想進B的last方法");
b.lashB();
}
public synchronized void lastA(){
Log.e("testthread",Thread.currentThread().getName()+"進入A的last方法");
}
}
public class B{
public synchronized void funB(A a){
Log.e("testthread",Thread.currentThread().getName()+"進入B的fun方法");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.e("testthread",Thread.currentThread().getName()+"想進A的last方法");
a.lastA();
}
public synchronized void lashB(){
Log.e("testthread",Thread.currentThread().getName()+"進入B的last方法");
}
}
public class DeadLock implements Runnable{
A a = new A();
B b = new B();
void init(){
Thread.currentThread().setName("主線程");
a.funA(b);
}
@Override
public void run() {
b.funB(a);
}
}
mBntFun5.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
DeadLock lock = new DeadLock();
new Thread(lock,"子線程").start();
lock.init();
}
});
代碼解釋:
因爲funA()和funB()都是同步方法,當a和b調用他們的時候,就要對a和b加鎖。而A、B中各自sleep(200)後,開始繼續執行,這是A中要調用B的lastB()方法,這時候就要對B加鎖,但是這時候B的鎖並沒有釋放,同理B中也是這樣的情況,所以雙方就一直在等待,造成了死鎖。
說明
suspend很容易造成死鎖,儘量不要使用它來暫停線程