java多執行緒之多執行緒的執行緒安全問題
什麼是執行緒安全問題?如何解決執行緒安全問題?
現在就進入正題,解答上述問題:
當多個執行緒同時操作同一個共享全域性變數的時候,就容易出現執行緒安全問題,執行緒安全問題只會影響到執行緒對同一個共享的全域性變數的寫操作。
接下來就為大家演示一個執行緒安全問題:
package com.java.threadSecurity;
/**
* @Author:Mr.Liu
* @Description:執行緒安全問題再現:火車站買票
* @Date:Created in 11:20 2017/12/5
* @Modified By:
*/
public class ThreadProblem implements Runnable{
private int ticketCount = 100;//火車站總票數
private int ticketNo = 100;//未賣票數量
@Override
public void run() {
while (ticketNo>0){
try {
Thread.sleep(100);//一些其他操作耗時100ms
} catch (InterruptedException e) {
e.printStackTrace();
}
buyTicket();
}
System.out.println(Thread.currentThread().getName()+"火車票已售罄" );
}
public void buyTicket(){
System.out.println(Thread.currentThread().getName()+"買票成功,當前是第幾張票?:"+(ticketCount-ticketNo+1));
ticketNo--;
}
public static void main(String[] args) {
System.out.println("火車站開始售票--------------start");
Runnable runnable = new ThreadProblem();
Thread t01 = new Thread(runnable,"售票視窗No.1");
Thread t02 = new Thread(runnable,"售票視窗No.2");
t01.start();
t02.start();
}
}
上述程式碼執行的結果會出現如下情況:
火車站開始售票--------------start
.
.
.
售票視窗No.2買票成功,當前是第幾張票?:95
售票視窗No.1買票成功,當前是第幾張票?:96
售票視窗No.2買票成功,當前是第幾張票?:97
售票視窗No.1買票成功,當前是第幾張票?:98
售票視窗No.2買票成功,當前是第幾張票?:99
售票視窗No.1買票成功,當前是第幾張票?:100
售票視窗No.1火車票已售罄
售票視窗No.2買票成功,當前是第幾張票?:101
售票視窗No.2火車票已售罄
從結果可以看出,火車站的已賣票數量比火車站的總票數還要大,這肯定是不合理的,原因就是因為兩個執行緒對共享的變數ticketNo同時做減票的操作,當ticketNo =1的時候,兩個執行緒同時滿足了while迴圈的條件,其中一個執行緒已經把ticketNo更新到了0,而另一條執行緒直接走到了buyTicket(),故此,出現了賣出第101張票。
那麼我們該如何解決呢?
對於這種現成安全問題我們通常採用鎖的機制,即:當一條執行緒在操作ticketNo的時候為這一段寫操作加上一把鎖,這樣其他執行緒就不能對這個ticketNo也做寫的操作,只有當這段寫操作的鎖被釋放之後才能由其他執行緒繼續做寫操作。接下來為大家講解synchronized同步鎖來解決上述執行緒安全問題,更改後的程式碼如下:
public void buyTicket(){
synchronized (ticketCount){
if(ticketNo>0){
System.out.println(Thread.currentThread().getName()+"買票成功,當前是第幾張票?:"+(ticketCount-ticketNo+1));
ticketNo--;
}
}
}
再次執行程式碼,可以看到:
火車站開始售票--------------start
.
.
.
售票視窗No.2買票成功,當前是第幾張票?:94
售票視窗No.1買票成功,當前是第幾張票?:95
售票視窗No.2買票成功,當前是第幾張票?:96
售票視窗No.1買票成功,當前是第幾張票?:97
售票視窗No.2買票成功,當前是第幾張票?:98
售票視窗No.1買票成功,當前是第幾張票?:99
售票視窗No.2買票成功,當前是第幾張票?:100
售票視窗No.2火車票已售罄
售票視窗No.1火車票已售罄
此時很明顯,已經不會出現最初的安全問題了。
synchronized除了上述用法之外還有如下兩種用法來達到共享全域性變數的同步保證同一時間只會有一個執行緒對這個共享全域性變數做操作,保證執行緒安全:
//同步函式1
public synchronized void buyTicket(){
if(ticketNo>0){
System.out.println(Thread.currentThread().getName()+"買票成功,當前是第幾張票?:"+(ticketCount-ticketNo+1));
ticketNo--;
}
}
//同步函式2
public static synchronized void buyTicket(){
if(ticketNo>0){
System.out.println(Thread.currentThread().getName()+"買票成功,當前是第幾張票?:"+(ticketCount-ticketNo+1));
ticketNo--;
}
}
上述用法同樣可以保證不會出現執行緒安全問題,這三種用法的區別在於:
1. synchronized包裹的程式碼塊所用的鎖可以是一個普通的共享物件;
2. 同步函式(public synchronized void buyTicket())所用的是this鎖;
3. 靜態同步函式(public static synchronized void buyTicket())所持有的是當前類載入的時候的位元組碼檔案物件(即上述程式碼的ThreadProblem.class)
同步鎖一般試用在傳統專案,且至少會有兩個執行緒同時操作資料,其效能不是特別好。
解決執行緒安全問題的另一種同步方法還有Lock鎖,Lock鎖與synchronized鎖的區別:
Lock鎖類似於手動擋汽車,獲取鎖和釋放鎖需要我們自己用程式碼操作,synchronized鎖類似自動擋汽車,鎖的獲取和釋放都是自動的,不需要人為去控制。相比之下Lock鎖更靈活。
Lock鎖常用API:
tryLock(long time, TimeUnit unit):嘗試在一定的時間內獲取鎖,如果超時未獲得鎖返回false,可能會拋 出InterruptedException異常;
tryLock():嘗試獲取鎖,如果無法獲取則返回false;
unlock():釋放所持有的鎖。
用法如下:
package com.java.threadSecurity;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author:Mr.Liu
* @Description:執行緒安全問題再現:火車站買票
* @Date:Created in 11:20 2017/12/5
* @Modified By:
*/
public class ThreadLock implements Runnable{
private int ticketCount = 100;//火車站總票數
private int ticketNo = 100;//未賣票數量
private Lock lock = new ReentrantLock();
@Override
public void run() {
while (ticketNo>0){
try {
Thread.sleep(100);//一些其他操作耗時100ms
} catch (InterruptedException e) {
e.printStackTrace();
}
buyTicketWithTimeLock();
//buyTicketWithNoTimeLock();
}
System.out.println(Thread.currentThread().getName()+"火車票已售罄");
}
//tryLock(long time, TimeUnit unit)
public void buyTicketWithTimeLock(){
boolean lockFalg = true;
try {
System.out.println(Thread.currentThread().getName()+"獲取鎖");
lockFalg = lock.tryLock(5, TimeUnit.SECONDS);//嘗試在5s內獲取鎖
if(lockFalg){//嘗試獲取鎖
if(ticketNo>0){
Thread.sleep(10000);//執行緒休眠10s
System.out.println(Thread.currentThread().getName()+"買票成功,當前是第幾張票?:"+(ticketCount-ticketNo+1));
ticketNo--;
}
}else{
System.out.println(Thread.currentThread().getName()+"獲取鎖失敗");
}
} catch (InterruptedException e) {
System.out.println(e.getCause());
} finally {
if(lockFalg){
System.out.println(Thread.currentThread().getName()+"釋放鎖");
lock.unlock();//釋放鎖
}
}
}
//tryLock()
public void buyTicketWithNoTimeLock(){
boolean flag = lock.tryLock();
if(flag){
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(ticketNo>0){
System.out.println(Thread.currentThread().getName()+"買票成功,當前是第幾張票?:"+(ticketCount-ticketNo+1));
ticketNo--;
}
System.out.println(Thread.currentThread().getName()+"釋放鎖");
lock.unlock();
}else{
System.out.println(Thread.currentThread().getName()+"獲取鎖失敗");
}
}
public static void main(String[] args) {
System.out.println("ThreadLock火車站開始售票--------------start");
Runnable runnable = new ThreadLock();
Thread t01 = new Thread(runnable,"售票視窗No.1");
Thread t02 = new Thread(runnable,"售票視窗No.2");
t01.start();
t02.start();
}
}
至此,Lock鎖和synchronized鎖解決執行緒安全問題已講解完畢。
下一篇將為大家講解多執行緒的三大特性。