1、一個典型的Java線程安全例子
package com.chanshuyi.thread;
public class ThreadDemo93 {
public static void main(String[] args) {
Account account = new Account(2300);
new DrawMoneyThread(account).start();
new DepositeThread(account).start();
}
}
class DepositeThread extends Thread{
private Account account;
public DepositeThread(Account account){
this.account = account;
}
@Override
public void run() {
//每次存200,10次共存2000
for(int i = 0; i < 10; i++){
account.deposit(200, i + 1);
}
}
}
class DrawMoneyThread extends Thread{
private Account account;
public DrawMoneyThread(Account account){
this.account = account;
}
@Override
public void run() {
//每次取100,10次共取1000
for(int i = 0; i < 10; i++){
account.withdraw(100, i + 1);
}
}
}
class Account{
//存錢
public void deposit(double amount, int i){
try {
Thread.sleep((long)Math.random()*10000); //模擬存錢的延遲
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
this.balance = this.balance + amount;
System.out.println("第" + i + "次,存入錢:" + amount);
System.out.println("第" + i + "次,存錢後賬戶餘額:" + this.balance);
}
//取錢
public void withdraw(double amount, int i){
if(this.balance >= amount){
try {
Thread.sleep((long)Math.random()*10000); //模擬取錢的延遲
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
this.balance = this.balance - amount;
System.out.println("第" + i + "次,取出錢:" + amount);
System.out.println("第" + i + "次,取錢後賬戶餘額:" + this.balance);
}else{
System.out.println("第" + i + "次,餘額不足");
}
}
public Account(){
}
public Account(double balance){
this.balance = balance;
}
private double balance;
}
上面例子很容易理解,有一張銀行卡,裏面有2300的餘額,程序模擬兩個人進行操作,一個人存10次錢每次存200共存2000,一個人取錢取10次每次取100共取1000,這樣的話最後的餘額應該是3300。多次運行此程序,可能具有多個不同組合的輸出結果。其中一種可能的輸出爲:
第1次,取出錢:100.0
第1次,取錢後賬戶餘額:2200.0
第1次,存入錢:200.0
第1次,存錢後賬戶餘額:2400.0
第2次,取出錢:100.0
第2次,取錢後賬戶餘額:2300.0
第2次,存入錢:200.0
第2次,存錢後賬戶餘額:2500.0
第3次,取出錢:100.0
第3次,取錢後賬戶餘額:2400.0
第3次,存入錢:200.0
第3次,存錢後賬戶餘額:2600.0
第4次,取出錢:100.0
第4次,取錢後賬戶餘額:2500.0
第4次,存入錢:200.0
第4次,存錢後賬戶餘額:2700.0
第5次,取出錢:100.0
第5次,取錢後賬戶餘額:2600.0
第5次,存入錢:200.0
第5次,存錢後賬戶餘額:2800.0
第6次,取出錢:100.0
第6次,取錢後賬戶餘額:2700.0
第6次,存入錢:200.0
第6次,存錢後賬戶餘額:2900.0
第7次,取出錢:100.0
第7次,取錢後賬戶餘額:2800.0
第7次,存入錢:200.0
第7次,存錢後賬戶餘額:3000.0
第8次,存入錢:200.0
第8次,取出錢:100.0
第8次,存錢後賬戶餘額:2900.0
第8次,取錢後賬戶餘額:2900.0
第9次,存入錢:200.0
第9次,存錢後賬戶餘額:3100.0
第9次,取出錢:100.0
第9次,取錢後賬戶餘額:3000.0
第10次,存入錢:200.0
第10次,存錢後賬戶餘額:3200.0
第10次,取出錢:100.0
第10次,取錢後賬戶餘額:3100.0
我們可以看到在第8次存錢和取錢的時候,本來之前的餘額是3000元,兩個人一個存入200,一個取出100,那麼餘額應該是3100纔是。但是因爲發生了兩人幾乎同時進行存取款操作,導致最後第8次存取款之後餘額進程是2900元。經過分析,問題在於Java多線程環境下的執行的不確定性。在存取款的時候,我們應該保證同一賬戶下不能同時進行存錢和取款操作,否則就會出現數據的混亂。而如果要保證存取款不能同時進行,就需要用到線程中的同步知識。
一般來說,實現線程同步的方式有:synchronized同步方法、synchronized同步代碼塊以及Lock鎖三種,這裏我們先介紹前兩種,Lock鎖的同步方式我們在下篇文章中介紹。
2、synchronized 同步方法
使用synchronized同步方法對線程同步,只需要在方法上synchronized關鍵字修飾即可。
上面的例子使用synchronized同步方法進行線程同步後的代碼如下:
package com.chanshuyi.thread;
public class ThreadDemo93 {
public static void main(String[] args) {
Account account = new Account(2300);
new DrawMoneyThread(account).start();
new DepositeThread(account).start();
}
}
class DepositeThread extends Thread{
private Account account;
public DepositeThread(Account account){
this.account = account;
}
@Override
public void run() {
//每次存200,10次共存2000
for(int i = 0; i < 10; i++){
account.deposit(200, i + 1);
}
}
}
class DrawMoneyThread extends Thread{
private Account account;
public DrawMoneyThread(Account account){
this.account = account;
}
@Override
public void run() {
//每次取100,10次共取1000
for(int i = 0; i < 10; i++){
account.withdraw(100, i + 1);
}
}
}
class Account{
//存錢
public synchronized void deposit(double amount, int i){
try {
Thread.sleep((long)Math.random()*10000); //模擬存錢的延遲
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
this.balance = this.balance + amount;
System.out.println("第" + i + "次,存入錢:" + amount);
System.out.println("第" + i + "次,存錢後賬戶餘額:" + this.balance);
}
//取錢
public synchronized void withdraw(double amount, int i){
if(this.balance >= amount){
try {
Thread.sleep((long)Math.random()*10000); //模擬取錢的延遲
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
this.balance = this.balance - amount;
System.out.println("第" + i + "次,取出錢:" + amount);
System.out.println("第" + i + "次,取錢後賬戶餘額:" + this.balance);
}else{
System.out.println("第" + i + "次,餘額不足");
}
}
public Account(){
}
public Account(double balance){
this.balance = balance;
}
private double balance;
}
運行上面的代碼,你會發現無論運行多少次,最終的餘額都是3300元,不會發生錯誤。
3、synchronized 同步代碼塊
上面的例子用synchronized同步代碼塊方式實現線程同步後的代碼如下:
package com.chanshuyi.thread;
public class ThreadDemo93 {
public static void main(String[] args) {
Account account = new Account(2300);
new DrawMoneyThread(account).start();
new DepositeThread(account).start();
}
}
class DepositeThread extends Thread{
private Account account;
public DepositeThread(Account account){
this.account = account;
}
@Override
public void run() {
//每次存200,10次共存2000
for(int i = 0; i < 10; i++){
account.deposit(200, i + 1);
}
}
}
class DrawMoneyThread extends Thread{
private Account account;
public DrawMoneyThread(Account account){
this.account = account;
}
@Override
public void run() {
//每次取100,10次共取1000
for(int i = 0; i < 10; i++){
account.withdraw(100, i + 1);
}
}
}
class Account{
//存錢
public void deposit(double amount, int i){
try {
Thread.sleep((long)Math.random()*10000); //模擬存錢的延遲
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized(this){
this.balance = this.balance + amount;
System.out.println("第" + i + "次,存入錢:" + amount);
System.out.println("第" + i + "次,存錢後賬戶餘額:" + this.balance);
}
}
//取錢
public synchronized void withdraw(double amount, int i){
try {
Thread.sleep((long)Math.random()*10000); //模擬取錢的延遲
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized(this){
if(this.balance >= amount){
this.balance = this.balance - amount;
System.out.println("第" + i + "次,取出錢:" + amount);
System.out.println("第" + i + "次,取錢後賬戶餘額:" + this.balance);
}else{
System.out.println("第" + i + "次,餘額不足");
}
}
}
public Account(){
}
public Account(double balance){
this.balance = balance;
}
private double balance;
}
通過同步代碼塊方式需要傳入一個同步對象,這個對象必須是唯一的,這樣才能實現同步。在synchronized(this)中我們傳入的對象this,其實就是main方法中聲明的Account對象。同樣的,運行上面的代碼,我們會發現每次的餘額都是3300,無論多少次都是一樣。
有沒有發現我們上面的例子中,每次賬戶的餘額都是2300,但這次我們把賬戶的初始餘額改成0,但是還是存10次200的,取20次100的,看看這次最終的餘額會不會是1000。
package com.chanshuyi.thread.part3.part32;
/**
* 銀行存取款 - 使用synchronized關鍵字修飾方法實現線程同步
* 實現效果:存取不能同步進行,但可能出現連續幾次存或連續幾次取
* @author yurongchan
*
*/
public class ThreadDemo1 {
public static void main(String[] args) {
Account account = new Account(0);
new DrawMoneyThread(account).start();
new DepositeThread(account).start();
}
}
class DepositeThread extends Thread{
private Account account;
public DepositeThread(Account account){
this.account = account;
}
@Override
public void run() {
//每次存200,10次共存2000
for(int i = 0; i < 10; i++){
account.deposit(200, i + 1);
}
}
}
class DrawMoneyThread extends Thread{
private Account account;
public DrawMoneyThread(Account account){
this.account = account;
}
@Override
public void run() {
//每次取100,10次共取1000
for(int i = 0; i < 10; i++){
account.withdraw(100, i + 1);
}
}
}
class Account{
//存錢
public synchronized void deposit(double amount, int i){
try {
Thread.sleep((long)Math.random()*10000); //模擬存錢的延遲
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
this.balance = this.balance + amount;
System.out.println("第" + i + "次,存入錢:" + amount);
System.out.println("第" + i + "次,存錢後賬戶餘額:" + this.balance);
}
//取錢
public synchronized void withdraw(double amount, int i){
if(this.balance >= amount){
try {
Thread.sleep((long)Math.random()*10000); //模擬取錢的延遲
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
this.balance = this.balance - amount;
System.out.println("第" + i + "次,取出錢:" + amount);
System.out.println("第" + i + "次,取錢後賬戶餘額:" + this.balance);
}else{
System.out.println("第" + i + "次,餘額不足");
}
}
public Account(){
}
public Account(double balance){
this.balance = balance;
}
private double balance;
}
運行上面的代碼,我們會發現有時候最終的餘額有時候並不是1000。那是因爲發生了同時存取款的情況嗎?不會呀,我們已經用synchronized關鍵字進行線程同步了。那究竟是什麼原因呢?仔細察看輸出信息我們可以發現有好幾次取款的時候發生了餘額不足的情況,也就是說我們再餘額爲0的時候發生了取款行爲,這時候取款當然就會失敗了。所以最終餘額錯誤是因爲我們忽略了餘額爲0的這種情況,正確的做法是當餘額爲0的時候,取款線程放棄鎖對象並進入等待狀態,等待存錢線程存錢之後進行喚醒。那這就涉及到了線程之間的通信了,在兩個線程之間進行通信,我們可以使用wait()和notify()進行通信。
關於傳入的鎖對象
使用synchronized方法實現線程同步,它使用的是synchronized類所在的內部對象,也就是該類的實例化對象作爲唯一的鎖對象。而使用synchronized代碼塊實現線程同步,可以傳進各種對象,只要你保證你在競爭的兩個線程中使用的是同一個對象就可以了。例如:使用synchronized(this)傳入的就是調用本類的那個類對象,即Account對象,在本例中就是在main方法中聲明的account對象。使用synchronized(String.class)就是使用String的字節類對象作爲鎖,這個對象也是絕對唯一的。在deposit()和withdraw中分別使用synchronized(“11”)其結果也是同步的,因爲鎖對象其實都是指向字符串池中唯一的一個"11"的字符串對象。如果看不懂,沒關係,下一篇文章會也會講解這個,到時候再回來瞭解一下就可以了。
4、使用wait()/notify()實現線程間通信
將上面的代碼稍微修改,使用wait()/notify()進行通信:
package com.chanshuyi.thread.part3.part34;
/**
* 銀行存取款 - 用synchronized實現線程同步,用wait()/notify()實現線程通信
* 實現效果:一次存,一次取,一直這樣直到結束,不會出現連續幾次存或取的情況
* @author yurongchan
*
*/
public class ThreadDemo1 {
public static void main(String[] args) {
Account account = new Account(0);
new DrawMoneyThread(account).start();
new DepositeThread(account).start();
}
}
class DepositeThread extends Thread{
private Account account;
public DepositeThread(Account account){
this.account = account;
}
@Override
public void run() {
//每次存200,10次共存2000
for(int i = 0; i < 10; i++){
account.deposit(200, i + 1);
//模擬存款的時間間隔
try {
Thread.sleep((long)Math.random()*5);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
class DrawMoneyThread extends Thread{
private Account account;
public DrawMoneyThread(Account account){
this.account = account;
}
@Override
public void run() {
//每次取100,10次共取1000
for(int i = 0; i < 10; i++){
account.withdraw(100, i + 1);
//模擬取款的時間間隔
try {
Thread.sleep((long)Math.random()*5);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
class Account{
//存款
public synchronized void deposit(double amount, int i){
System.out.println("***存款線程" + i + "開始存款.");
try {
Thread.sleep((long)Math.random()*10000); //模擬存款的延遲
this.balance = this.balance + amount;
System.out.println("***第" + i + "次,存入錢:" + amount);
System.out.println("***第" + i + "次,存款後賬戶餘額:" + this.balance);
notifyAll(); //喚醒所有存款進程
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//取款
public synchronized void withdraw(double amount, int i){
while(this.balance < amount){
try {
System.out.println("---取款線程" + i + "取款時發生餘額不足.放棄對象鎖,進入Lock Block.");
wait(); //餘額不足,等待
System.out.println("---取款線程" + i + "被喚醒,嘗試取款操作.");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("---取款線程" + i + "開始存款.");
try {
Thread.sleep((long)Math.random()*10000); //模擬取款的延遲
} catch (InterruptedException e) {
e.printStackTrace();
}
this.balance = this.balance - amount;
System.out.println("---第" + i + "次,取出錢:" + amount);
System.out.println("---第" + i + "次,取款後賬戶餘額:" + this.balance);
}
public Account(){
}
public Account(double balance){
this.balance = balance;
}
private double balance;
}
在上面的例子中,我們再取款之前先判斷賬戶餘額是否足夠,如果餘額不足則讓線程讓出對象鎖並等待(調用wait()方法會讓線程讓出對象鎖)。而當有存款線程進行存款操作時,存款線程最後會喚醒所有休眠的線程,讓他們嘗試去取款。下面是其中一個輸出:
###取款線程1取款時發生餘額不足.放棄對象鎖,進入Lock Block.
***存款線程1開始存款.
***第1次,存入錢:200.0
***第1次,存款後賬戶餘額:200.0
###取款線程1被喚醒,嘗試取款操作.
---取款線程1開始存款.
---第1次,取出錢:100.0
---第1次,取款後賬戶餘額:100.0
***存款線程2開始存款.
***第2次,存入錢:200.0
***第2次,存款後賬戶餘額:300.0
---取款線程2開始存款.
---第2次,取出錢:100.0
---第2次,取款後賬戶餘額:200.0
***存款線程3開始存款.
***第3次,存入錢:200.0
***第3次,存款後賬戶餘額:400.0
---取款線程3開始存款.
---第3次,取出錢:100.0
---第3次,取款後賬戶餘額:300.0
***存款線程4開始存款.
***第4次,存入錢:200.0
***第4次,存款後賬戶餘額:500.0
---取款線程4開始存款.
---第4次,取出錢:100.0
---第4次,取款後賬戶餘額:400.0
***存款線程5開始存款.
***第5次,存入錢:200.0
***第5次,存款後賬戶餘額:600.0
---取款線程5開始存款.
---第5次,取出錢:100.0
---第5次,取款後賬戶餘額:500.0
***存款線程6開始存款.
***第6次,存入錢:200.0
***第6次,存款後賬戶餘額:700.0
---取款線程6開始存款.
---第6次,取出錢:100.0
---第6次,取款後賬戶餘額:600.0
***存款線程7開始存款.
***第7次,存入錢:200.0
***第7次,存款後賬戶餘額:800.0
---取款線程7開始存款.
---第7次,取出錢:100.0
---第7次,取款後賬戶餘額:700.0
***存款線程8開始存款.
***第8次,存入錢:200.0
***第8次,存款後賬戶餘額:900.0
---取款線程8開始存款.
---第8次,取出錢:100.0
---第8次,取款後賬戶餘額:800.0
***存款線程9開始存款.
***第9次,存入錢:200.0
***第9次,存款後賬戶餘額:1000.0
***存款線程10開始存款.
***第10次,存入錢:200.0
***第10次,存款後賬戶餘額:1200.0
---取款線程9開始存款.
---第9次,取出錢:100.0
---第9次,取款後賬戶餘額:1100.0
---取款線程10開始存款.
---第10次,取出錢:100.0
---第10次,取款後賬戶餘額:1000.0
從上面的輸出我們可以看到一開始的時候第一個取款的線程嘗試去取款,但是餘額不足,於是它放棄了對象鎖並進入阻塞狀態。之後存款線程1獲得了對象鎖,並往賬戶存入了200,最後調用了notifyAll()方法喚醒了所有的取款線程。此時取款線程1被喚醒,它嘗試着繼續去取款,判斷髮現確實賬戶有餘額,於是就進行取款操作。
講到這裏,相信大部分人都會對synchronized和wait()/notify()的作用有一個感性的瞭解。synchronized只負責實現線程同步,而wait()/notify()方法可以幫助線程在線程同步的基礎上實現線程通信,從而實現更加負責的功能。
Synchronized 關鍵字作用域
synchronized關鍵字的作用域有二種:
- 1)是某個對象實例內,synchronized
aMethod(){}可以防止多個線程同時訪問這個對象的synchronized方法(如果一個對象有多個synchronized方法,只要一個線程訪問了其中的一個synchronized方法,其它線程不能同時訪問這個對象中任何一個synchronized方法)。這時,不同的對象實例的
synchronized方法是不相干擾的。也就是說,其它線程照樣可以同時訪問相同類的另一個對象實例中的synchronized方法; - 2)是某個類的範圍,synchronized static
aStaticMethod{}防止多個線程同時訪問這個類中的synchronized static 方法(因爲此時用的是 對象.class
作爲鎖)。它可以對類的所有對象實例起作用。