Java之線程安全中的三種同步方式


一個程序在運行起來時,會轉換為進程,通常含有多個線程。

通常情況下,一個進程中的比較耗時的操作(如長循環、文件上傳下載、網絡資源獲取等),往往會采用多線程來解決。

比如,現實生活中,銀行取錢問題、火車票多個窗口售票問題等,通常會涉及並發問題,從而需要用到多線程技術。

當進程中有多個並發線程進入一個重要數據的代碼塊時,在修改數據的過程中,很有可能引發線程安全問題,從而造成數據異常。例如,正常邏輯下,同一個編號的火車票只能售出一次,卻由於線程安全問題而被多次售出,從而引起實際業務異常。

接下來,我以售票問題,來演示多線程問題中對核心數據保護的重要性。我們先來看不對多線程數據進行保護時會引發什么樣的狀況。

/**
* 售票問題
*/
public class Test1 {
static int tickets=10;
class SellTickets implements Runnable{
@Override
public void run() {
// 未加同步時,產生臟數據
while(tickets>0){
System.out.println(Thread.currentThread().getName()+" -->售出第 "+tickets+" 張票");
tickets--;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if(tickets<=0){
System.out.println(Thread.currentThread().getName()+" -->售票結束!");
}
}
}
public static void main(String[] args) {
SellTickets sell=new Test1().new SellTickets();
Thread t1=new Thread(sell, "1號窗口");
Thread t2=new Thread(sell, "2號窗口");
Thread t3=new Thread(sell, "3號窗口");
Thread t4=new Thread(sell, "4號窗口");
t1.start();
t2.start();
t3.start();
t4.start();
}
}

  上述代碼運行后,效果如下:

1號窗口 -->售出第 10 張票
3號窗口 -->售出第 10 張票
2號窗口 -->售出第 10 張票
4號窗口 -->售出第 10 張票
3號窗口 -->售出第 6 張票
2號窗口 -->售出第 6 張票
1號窗口 -->售出第 5 張票
4號窗口 -->售出第 3 張票
3號窗口 -->售出第 2 張票
2號窗口 -->售出第 2 張票
1號窗口 -->售出第 2 張票
4號窗口 -->售票結束!
3號窗口 -->售票結束!
1號窗口 -->售票結束!
2號窗口 -->售票結束!

  上述運行結果中,第10張票被售出多次,顯然不符合實際應用中的邏輯。由於多線程調度中的不確定性,讀者在演示上述代碼時,可能會取得不同的運行結果。

  為了解決上述臟數據的問題,我為大家介紹3種使用比較普遍的三種同步方式。

  第一種,同步代碼塊。

  有synchronized關鍵字修飾的語句塊,即為同步代碼塊。同步代碼塊會被JVM自動加上內置鎖,從而實現同步。

  我們來看代碼:

/**
* 售票問題
* @author 李章勇
*
*/
public class Test2 {
static int tickets=10;
class SellTickets implements Runnable{
@Override
public void run() {
//同步代碼塊
while(tickets>0){
synchronized(this){
if(tickets<=0){
break;
}
System.out.println(Thread.currentThread().getName()+" -->售出第 "+tickets+" 張票");
tickets--;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if(tickets<=0){
System.out.println(Thread.currentThread().getName()+" -->售票結束!");
}
}
}
public static void main(String[] args) {
SellTickets sell=new Test2().new SellTickets();
Thread t1=new Thread(sell, "1號窗口");
Thread t2=new Thread(sell, "2號窗口");
Thread t3=new Thread(sell, "3號窗口");
Thread t4=new Thread(sell, "4號窗口");
t1.start();
t2.start();
t3.start();
t4.start();
}
}

 上述代碼運行結果:

1號窗口 -->售出第 10 張票
3號窗口 -->售出第 9 張票
4號窗口 -->售出第 8 張票
2號窗口 -->售出第 7 張票
3號窗口 -->售出第 6 張票
4號窗口 -->售出第 5 張票
2號窗口 -->售出第 4 張票
1號窗口 -->售出第 3 張票
4號窗口 -->售出第 2 張票
3號窗口 -->售出第 1 張票
1號窗口 -->售票結束!
2號窗口 -->售票結束!
4號窗口 -->售票結束!
3號窗口 -->售票結束!

  通過運行結果可知,上述運行結果正常。

  第二種,同步方法 。

  即有synchronized關鍵字修飾的方法。由於java的每個對象都有一個內置鎖,當用此關鍵字修飾方法時,內置鎖會保護整個方法。在調用該方法前,需要獲得內置鎖,否則就處於阻塞狀態。

  我們來看代碼:

/**
* 售票問題
* @author 李章勇
*
*/
public class Test3 {
static int tickets=10;
class SellTickets implements Runnable{
@Override
public void run() {
//同步方法
while(tickets>0){
synMethod();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if(tickets<=0){
System.out.println(Thread.currentThread().getName()+" -->售票結束!");
}
}
//同步方法
synchronized void synMethod(){
synchronized(this){
if(tickets<=0){
return;
}
System.out.println(Thread.currentThread().getName()+" -->售出第 "+tickets+" 張票");
tickets--;
}
}
}
public static void main(String[] args) {
SellTickets sell=new Test3().new SellTickets();
Thread t1=new Thread(sell, "1號窗口");
Thread t2=new Thread(sell, "2號窗口");
Thread t3=new Thread(sell, "3號窗口");
Thread t4=new Thread(sell, "4號窗口");
t1.start();
t2.start();
t3.start();
t4.start();
}
}

  上述代碼運行結果:

1號窗口 -->售出第 10 張票
4號窗口 -->售出第 9 張票
3號窗口 -->售出第 8 張票
2號窗口 -->售出第 7 張票
1號窗口 -->售出第 6 張票
2號窗口 -->售出第 5 張票
4號窗口 -->售出第 4 張票
3號窗口 -->售出第 3 張票
4號窗口 -->售出第 2 張票
3號窗口 -->售出第 1 張票
1號窗口 -->售票結束!
4號窗口 -->售票結束!
2號窗口 -->售票結束!
3號窗口 -->售票結束!

  上述代碼運行結果也正常。

  第三種,Lock鎖機制。

  通過創建Lock對象,采用lock()加鎖,采用unlock()解鎖,來保護指定代碼塊。我們看如下代碼:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 售票問題
* @author 李章勇
*
*/
public class Test4 {
static int tickets=10;
class SellTickets implements Runnable{
Lock lock=new ReentrantLock();
@Override
public void run() {
//Lock鎖機制
while(tickets>0){
try{
lock.lock();
if(tickets<=0){
break;
}
System.out.println(Thread.currentThread().getName()+" -->售出第 "+tickets+" 張票");
tickets--;
}finally{
lock.unlock();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
if(tickets<=0){
System.out.println(Thread.currentThread().getName()+" -->售票結束!");
}
}
}
public static void main(String[] args) {
SellTickets sell=new Test4().new SellTickets();
Thread t1=new Thread(sell, "1號窗口");
Thread t2=new Thread(sell, "2號窗口");
Thread t3=new Thread(sell, "3號窗口");
Thread t4=new Thread(sell, "4號窗口");
t1.start();
t2.start();
t3.start();
t4.start();
}
}

  運行結果如下:

1號窗口 -->售出第 10 張票
2號窗口 -->售出第 9 張票
3號窗口 -->售出第 8 張票
4號窗口 -->售出第 7 張票
1號窗口 -->售出第 6 張票
4號窗口 -->售出第 5 張票
2號窗口 -->售出第 4 張票
3號窗口 -->售出第 3 張票
1號窗口 -->售出第 2 張票
2號窗口 -->售出第 1 張票
3號窗口 -->售票結束!
1號窗口 -->售票結束!
2號窗口 -->售票結束!
4號窗口 -->售票結束!

  最后總結:

  由於synchronized是在JVM層面實現的,因此系統可以監控鎖的釋放與否;而ReentrantLock是使用代碼實現的,系統無法自動釋放鎖,需要在代碼中的finally子句中顯式釋放鎖lock.unlock()。

  另外,在並發量比較小的情況下,使用synchronized是個不錯的選擇;但是在並發量比較高的情況下,其性能下降會很嚴重,此時ReentrantLock是個不錯的方案。

  補充:  

  在使用synchronized 代碼塊時,可以與wait()、notify()、nitifyAll()一起使用,從而進一步實現線程的通信。
其中,wait()方法會釋放占有的對象鎖,當前線程進入等待池,釋放cpu,而其他正在等待的線程即可搶占此鎖,獲得鎖的線程即可運行程序;線程的sleep()方法則表示,當前線程會休眠一段時間,休眠期間,會暫時釋放cpu,但並不釋放對象鎖,也就是說,在休眠期間,其他線程依然無法進入被同步保護的代碼內部,當前線程休眠結束時,會重新獲得cpu執行權,從而執行被同步保護的代碼。
wait()和sleep()最大的不同在於wait()會釋放對象鎖,而sleep()不會釋放對象鎖。

  notify()方法會喚醒因為調用對象的wait()而處於等待狀態的線程,從而使得該線程有機會獲取對象鎖。調用notify()后,當前線程並不會立即釋放鎖,而是繼續執行當前代碼,直到synchronized中的代碼全部執行完畢,才會釋放對象鎖。JVM會在等待的線程中調度一個線程去獲得對象鎖,執行代碼。

  需要注意的是,wait()和notify()必須在synchronized代碼塊中調用。

  notifyAll()是喚醒所有等待的線程。

  接下來,我們通過下一個程序,使得兩個線程交替打印“A”和“B”各10次。請見下述代碼:

 

public class Test5 {
static final Object obj=new Object();
//一個子線程
static class ThreadA implements Runnable{
@Override
public void run() {
int count=10;
while(count>0){
synchronized(Test5.obj){
System.out.println("A-->"+count);
count--;
Test5.obj.notify();
try {
Test5.obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
//另一個子線程
static class ThreadB implements Runnable{
@Override
public void run() {
int count=10;
while(count>0){
synchronized(Test5.obj){
System.out.println("B-->"+count);
count--;
Test5.obj.notify();
try {
Test5.obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public static void main(String[] args) {
new Thread(new ThreadA()).start();
new Thread(new ThreadB()).start();
}
}

  顯示結果如下:

A-->10
B-->10
A-->9
B-->9
A-->8
B-->8
A-->7
B-->7
A-->6
B-->6
A-->5
B-->5
A-->4
B-->4
A-->3
B-->3
A-->2
B-->2
A-->1
B-->1

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM